চিজ ক্লাব

চিজ ক্লাব

লার্নিং অনুশীলনী

ভূমিকা

টেকনিক্যালি বলতে গেলে, Higher Order Function হলো এমন একটি ফাংশন যা নিচের অন্তত একটি কাজ করে:

  • এর আর্গুমেন্টগুলোর একটি হিসেবে একটি ফাংশন নেয়।
  • এর ফলাফল হিসেবে একটি ফাংশন রিটার্ন করে।

ফাংশনাল প্রোগ্রামিংয়ের জগতে অবশ্য এর ব্যবহার সাধারণত আরও সংকীর্ণ। সাধারণত এই শব্দটি filter, map আর reduce-এর মতো ফাংশনকে বোঝায়, যেগুলো একটি পাস করা ফাংশনকে একটি কলেকশনের প্রতিটি এলিমেন্টে প্রয়োগ করে।

কলেকশন নিয়ে কাজ করা

সিলেবাসের এই জায়গায় এসে আমরা ইতিমধ্যেই দেখেছি, একটি ইটারেবল কলেকশনের সব এলিমেন্টে, যেমন একটি Vector-এ, কোনো অপারেশন প্রয়োগ করার বিভিন্ন উপায় আছে:

  • একটি লুপ ব্যবহার করা (ডিজিটাল কম্পিউটিংয়ের সূচনা লগ্ন থেকে বেশিরভাগ প্রোগ্রামিং ভাষা যেভাবে করে)।
  • একটি কম্প্রিহেনশন ব্যবহার করা (Python-স্টাইলে)।
  • ব্রডকাস্টিং ব্যবহার করা (Julia-র নিজস্ব সিনট্যাক্স, যদিও R, Matlab আর NumPy-র কাছে এর বিশাল ঋণ আছে)।

এই কনসেপ্টে আমরা মূলত হায়ার-অর্ডার ফাংশন নিয়ে আলোচনা করব (যে-কোনো ফাংশনাল ভাষা, যেমন Haskell বা F#-এর সাথে এগুলো পরিচিত)।

আরও কিছু সম্ভাব্য উপায় আছে:

  • রিকার্শন (ML পরিবারের ভাষাগুলোর মতো)।
    • Julia এটা করতে দেয়, কিন্তু টেইল-কল অপ্টিমাইজেশন ছাড়া স্ট্যাক ওভারফ্লোর ঝুঁকি থাকে।
  • ম্যাক্রো দিয়ে মেটাপ্রোগ্রামিং (ঐতিহ্যগতভাবে Lisp-এর একটি বৈশিষ্ট্য)।
    • উচ্চতর Julia প্রোগ্রামিংয়ে এটি ব্যাপকভাবে ব্যবহৃত হয়, তবে বেশিরভাগ ক্ষেত্রেই সাবধানে ব্যবহার করুন।
    • অন্য বিকল্পগুলো সাধারণত লিখতে সহজ এবং debug করাও অনেক সহজ।

ফিল্টারিং

filter() ফাংশনটি একটি বুলিয়ান রিটার্ন ভ্যালুসহ পাস করা একটি ফাংশন নেয় এবং সেটি একটি কলেকশনে প্রয়োগ করে। শুধু যেসব এলিমেন্ট true রিটার্ন করে সেগুলোই রিটার্ন ভ্যালুতে থাকে, আর এই রিটার্ন ভ্যালুটি ইনপুটের মতোই একই মূল টাইপের (নিচে দেখুন)।

julia> filter(iseven, 1:6)
3-element Vector{Int64}:
 2
 4
 6

# String is a collection of Chars, so String in -> String out
julia> filter(!isascii, "Hrōðgār")
"ōðā"

# tuple input -> tuple output
julia> filter(iseven, (1, 2, 3, 4, 5))
(2, 4)

মাল্টি-ডাইমেনশনাল অ্যারের ক্ষেত্রে filter ইনপুটের ডাইমেনশনগুলো চ্যাপ্টা করে দেয় আর একটি Vector রিটার্ন করে: আউটপুট টাইপ ইনপুট টাইপের সাথে মেলে, এমন যেকোনো নিয়মের প্রধান ব্যতিক্রম এটি।

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> filter(isodd, m)
3-element Vector{Int64}:
 1
 5
 3

উপরের উদাহরণগুলোতে বিল্ট-ইন ফাংশন ব্যবহার করা হয়েছে, তবে এই প্রসঙ্গে অ্যানোনিমাস ফাংশনের ব্যবহার খুবই সাধারণ।

julia> filter(x -> x % 3 == 0, 1:20)
6-element Vector{Int64}:
  3
  6
  9
 12
 15
 18

এই কনসেপ্টের অনেক ফাংশনের মতোই এরও একটি ইন-প্লেস সংস্করণ আছে, filter!()।

ম্যাপিং

map() ফাংশনটি একটি ফাংশনকে প্রতিটি এলিমেন্টে প্রয়োগ করে একটি কলেকশন রূপান্তর করে। সহজ ক্ষেত্রে এটি ব্রডকাস্টিংয়ের মতো হতে পারে, যেখানে আউটপুটের আকার ইনপুটের সাথে মেলে।

julia> map(√, [1, 4, 9])
3-element Vector{Float64}:
 1.0
 2.0
 3.0

julia> map(x -> x^2 + 1, 1:4)
4-element Vector{Int64}:
  2
  5
 10
 17

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> map(√, m)
2×3 Matrix{Float64}:
 1.0  1.41421  1.73205
 2.0  2.23607  2.44949

map() একাধিক কলেকশনের উপর এলিমেন্ট ধরে ধরে কাজও করতে পারে।

julia> map(*, [1, 2], [3, 4])
2-element Vector{Int64}:
 3
 8

ধারণাগতভাবে আমরা এটাকে এভাবে ভাবতে পারি: একাধিক ইনপুট কলেকশনের উপর zip() চালানো হলো, তারপর মধ্যবর্তী ফলাফলের প্রতিটি এলিমেন্টে map() চালানো হলো। এটা শুধুই একটি মোটামুটি উপমা, বাস্তবায়ন সম্পর্কে এতে কিছুই বোঝায় না!

zip()-এর মতোই, যেসব কলেকশনের আকার মেলে না সেগুলো সবচেয়ে ছোটটির ডাইমেনশন পর্যন্ত ছেঁটে দেওয়া হয়।

কখনো কখনো পাস করা ফাংশনের শুধু সাইড এফেক্টই দরকার হয়, যেমন ডেটাবেসে লেখা বা একটি অ্যারেতে push! করা। তখন হায়ার-অর্ডার ফাংশন foreach() ব্যবহার করতে পারেন, যা সবসময় nothing রিটার্ন করে।

রিডিউসিং

reduce() ফাংশনটি একটি ২-আর্গুমেন্টের ফাংশন নেয় এবং সেটি একটি কলেকশনে প্রয়োগ করে, যার ফলে ডাইমেনশন কমে যায়।

শুনতে হয়তো অস্পষ্ট লাগছে, তবে sum() বা prod()-এর মতো ফাংশনগুলোর কথা ভাবুন, যেগুলো একটি কলেকশন নেয় আর একটি মাত্র মান রিটার্ন করে।

julia> sum(1:4) # add
10

julia> prod(1:4) # multiply
24

এই বিশেষ ফাংশনগুলো খুব ভালোভাবে অপ্টিমাইজ করা, তাই যেখানে এগুলো আছে সেখানে সবসময় এগুলোই ব্যবহার করা উচিত। আরও উদাহরণের মধ্যে আছে maximum() আর minimum(), লজিক্যাল ফাংশন all() ও any(), এবং অনেক স্ট্যাটিস্টিক্যাল ফাংশন।

শুধু বোঝানোর জন্যই, ধরুন একই কাজ আরও জেনেরিক reduce() দিয়ে করা হচ্ছে (মনে রাখবেন, ইনফিক্স অপারেটর + আর * আসলে ভেতরে ভেতরে ফাংশনই)।

julia> reduce(+, 1:4) # add
10

julia> reduce(*, 1:4) # multiply
24

sum() ও অন্য অ্যাগ্রিগেশন ফাংশনগুলোর মতোই reduce() একটি ঐচ্ছিক কিওয়ার্ড আর্গুমেন্ট dims নিতে পারে, যা দিয়ে বোঝানো হয় কোন ডাইমենশন বা ডাইমেনশনগুলোতে রিডিউস করতে হবে।

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> reduce(+, m; dims=1)
1×3 Matrix{Int64}:
 5  7  9

এগুলো সহজ উদাহরণ, কারণ যোগ আর গুণ দুটোই কমিউটেটিভ (1+2 == 2+1) এবং অ্যাসোসিয়েটিভ ( (1+2)+3 == 1+(2+3) )।

কিন্তু এটা মোটেই সর্বজনীন নয়! এমনকি বিয়োগ আর ভাগের মতো কমন অপারেশনও অ্যাসোসিয়েটিভ নয়।

এর সাথে আরও একটি সমস্যা আছে: বড় কলেকশনে ফ্লোটিং-পয়েন্ট এরর জমতে পারে, তাই বাম থেকে ডানে রিডিউস করলে ডান থেকে বামের চেয়ে সামান্য আলাদা উত্তর আসতে পারে।

Julia-র reduce ফাংশনের দিক ইমপ্লিমেন্টেশনের উপর নির্ভর করে, এর কোনো নিশ্চয়তা নেই।

দিক স্পষ্টভাবে নিয়ন্ত্রণ করতে আছে foldl() আর foldr() ফাংশন, যেগুলো যথাক্রমে "বাম" ও "ডান" থেকে শুরু করে বলে ধরা হয় (একটি Vector-এর ক্ষেত্রে আসলে উপর ও নিচ থেকে)।

julia> foldl(-, 1:3) # (1 - 2) - 3
-4

julia> foldr(-, 1:3) # 1 - (2 - 3)
2

মনে রাখবেন, এগুলো এমন কলেকশনের জন্য বানানো যেগুলোকে এক-মাত্রিক হিসেবে ধরা যায় এবং যেগুলো একটি স্কেলার ফলাফল রিটার্ন করে। foldl আর foldr-এ dims আর্গুমেন্ট সমর্থিত নয়, শুধু reduce-এ সমর্থিত।

ম্যাপরিডিউস

map অপারেশনের সাথে reduce মেলানো বিভিন্ন প্রোগ্রামিং ক্ষেত্রে খুবই কমন।

আমরা চাইলে ক্রমানুসারে map চালিয়ে তারপর মধ্যবর্তী কলেকশনে reduce চালাতে পারি। তবে এটা ভালো-মন্দ মিলিয়ে অদক্ষ, আর কলেকশন বড় হওয়ার সাথে সাথে এর স্কেলিং আরও খারাপ হয়।

এর বদলে সম্মিলিত mapreduce() ফাংশনটি ব্যবহার করার জোরালো সুপারিশ করা হয়। এটি অনেক বেশি পারফরম্যান্ট একটি অ্যালগরিদম ব্যবহার করতে পারে, যা map আর reduce অপারেশনগুলো পালাক্রমে চালায়।

প্রথম আর্গুমেন্টটি হলো যে ফাংশন দিয়ে map করা হবে, আর দ্বিতীয় আর্গুমেন্টটি হলো reduce অপারেটর।

julia> mapreduce(x -> x^2 + 1, +, 1:3)
17

# equivalent to (2 + 5 + 10)
julia> sum(map(x -> x^2 + 1, 1:3))
17

আমরা যেমনটি আশা করতেই পারি, যেখানে দিক গুরুত্বপূর্ণ সেখানে Julia-র আছে mapfoldl() আর mapfoldr() ফাংশন।

নির্দেশনা

আমরা একটি চিজ ক্লাব শুরু করছি, যা আমাদের চিজপ্রেমী গ্রাহকদের ইতিহাস ও রুচির ভিত্তিতে তাঁদের জন্য নতুন চিজ বাছাই করতে ML ব্যবহার করবে।

নতুন সদস্যদের একটি প্রাথমিক সার্ভে পূরণ করতে হবে, যাতে আমরা শুরুর জন্য কিছু মৌলিক ডেটা সংগ্রহ করতে পারি। এর মাধ্যমে দেখা গেছে, এমন কিছু জোরালো ক্লায়েন্ট আছেন যাঁদের সমালোচনায় সূক্ষ্মতা কম। যেহেতু এটি আরও সূক্ষ্ম একটি অ্যালগরিদমকে অপূরণীয়ভাবে পক্ষপাতদুষ্ট করে তুলতে পারে, তাই তাঁদের চাহিদা সামলানোর জন্য আলাদা একটি অ্যালগরিদম তৈরি করা হয়েছে। আপনাকে তাঁদের ডেটা সামলানোর জন্য কিছু হেল্পার ফাংশন লিখতে বলা হয়েছে।

Note

নিচের কাজগুলো সমাধান করার বিভিন্ন উপায় থাকতে পারে, তবুও প্রতিটি আলাদা একটি একক হায়ার-অর্ডার ফাংশন দিয়ে সমাধান করা যায়।

1. গ্রাহকদের শ্রেণীবদ্ধ করুন

রেটিং সিস্টেমটি পাঁচ তারার ভিত্তিতে তৈরি, যা সহজ কথায় 1:5 ইন্টিজার নিয়ে গঠিত। জোরালো গ্রাহকেরা কেবল 1 বা 5 রেটিং দেবেন, এবং আমরা জানতে চাই কোনো গ্রাহক এই আচরণ দেখান কি না।

all_15() ইমপ্লিমেন্ট করুন, যা রেটিংয়ের একটি ভেক্টর নেয় এবং সব রেটিং 1 বা 5 হলে true রিটার্ন করে, আর অন্যথায় false।

julia> ratings = [2, 3, 4, 4, 1];

julia> all_15(ratings)
false

julia> ratings = [1, 5, 5, 1, 5];

julia> all_15(ratings)
true

2. জোরালো গ্রাহকদের আলাদা করুন

আমাদের আরও জোরালো গ্রাহকদের অন্যদের থেকে আলাদা করতে হবে।

emphatics() ইমপ্লিমেন্ট করুন, যা গ্রাহক ও রেটিংয়ের একটি ডিকশনারি নেয়। একই ধরনের একটি ডিকশনারি রিটার্ন করে, যেখানে কেবল 1 বা 5 তারার রেটিং ব্যবহারকারীরা থাকেন।

julia> ratings = ([2, 3, 5, 1, 1], [1, 1, 5, 5, 1], [4, 5, 5, 3, 2], [5, 5, 1, 1, 5]);

julia> names = ("c1", "c2", "c3", "c4");

julia> customers = Dict(zip(names, ratings))
Dict{String, Vector{Int64}} with 4 entries:
  "c2" => [1, 1, 5, 5, 1]
  "c1" => [2, 3, 5, 1, 1]
  "c3" => [4, 5, 5, 3, 2]
  "c4" => [5, 5, 1, 1, 5]

julia> emphatics(customers)
Dict{String, Vector{Int64}} with 2 entries:
  "c2" => [1, 1, 5, 5, 1]
  "c4" => [5, 5, 1, 1, 5]

3. রেটিং বাইনারিতে বদলান

যেহেতু জোরালো গ্রাহকেরা কেবল 1 ও 5 রেটিং ব্যবহার করেন, তাই এগুলোকে 0 ও 1-এ বদলে দিলে গণনার দিক থেকে সুবিধা হবে।

tobinary() ইমপ্লিমেন্ট করুন, যা জোরালো রেটিংয়ের একটি ভেক্টর নেয়। বাইনারি রেটিং রিটার্ন করে, যেখানে 1-কে 0-এ এবং 5-কে 1-এ বদলানো হয়েছে।

julia> ratings = [1, 1, 5, 5, 1];

julia> tobinary(ratings)
5-element Vector{Int64}:
 0
 0
 1
 1
 0

4. রেটিং থেকে ম্যাট্রিক্স তৈরি করুন

আমাদের অ্যালগরিদমগুলো Matrix ইনপুট ব্যবহার করে, তাই আমাদের ডেটাকে সেটিতে রূপান্তর করতে হবে।

tobinarymatrix() ইমপ্লিমেন্ট করুন, যা জোরালো রেটিং ভেক্টরের একটি ভেক্টর নেয়। রূপান্তরিত ডেটার একটি Matrix রিটার্ন করে, যেখানে প্রতিটি রেটিং ভেক্টর ম্যাট্রিক্সের একটি সারি।

julia> customersratings = [[1, 1, 5, 5, 1],[5, 5, 1, 1, 5]];

julia> tobinarymatrix(customersratings)
2×5 Matrix{Int64}:
 0  0  1  1  0
 1  1  0  0  1
GitHub-এর মাধ্যমে সম্পাদনা করুন লিংকটি একটি নতুন উইন্ডো বা ট্যাবে খোলে
Julia Exercism

চিজ ক্লাব শুরু করতে প্রস্তুত?

Exercism-এ সাইন আপ করুন, Julia ট্র্যাকের 35টি কনসেপ্ট128টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।