Điều này đặt ra một thách thức lớn: làm thế nào để bạn tối đa hóa phạm vi tiếp cận của chiến dịch đó thông qua mạng lưới các KOL Key Opinion Leader - Người có ảnh hưởng mà trường đã xác
Trang 1SỞ GIÁO DỤC VÀ ĐÀO TẠO
TRƯỜNG ĐẠI HỌC PHENIKAA
BÁO CÁO BÀI TẬP LỚN CUỐI KÌ MÔN ỨNG DỤNG THUẬT TOÁN
Topic: Influence Maximization with Limited Budget
Giảng viên hướng dẫn: Nguyễn Minh Anh
Thành viên nhóm 5:
Trần Văn Việt - 21010561Nguyễn Xuân Phước - 21013187Bùi Vương Trưởng - 21011230Trần Đức Toản - 21011228Nguyễn Thế Nam - 21010632Nguyễn Thu Phương - 21011231
HÀ NỘI, NĂM 2023
Trang 2MỤC LỤC
CHƯƠNG 1: GIỚI THIỆU 3
CHƯƠNG 2: PHÂN TÍCH VÀ GIẢI QUYẾT VẤN ĐỀ 5
A Data 5
B Tạo lời giải đầu 5
1 Hàm firstSolution() 5
2 Hàm firstHeristic() 6
3 Hàm MasterFirstSolution() 7
4 Hàm Putting() 8
C Các hàm cải thiện 8
1 Hàm localsearch() 8
2 Hàm reset() 10
3 Hàm fillSolution() 11
D Kết quả 12
1 Kết quả trong 1 data 12
2 Kết quả giữa data với nhau 13
CHƯƠNG 3: PHỤ LỤC 14
A Phân chia công việc 14
B Hình ảnh 14
C Xem thêm 14
Trang 3CHƯƠNG 1: GIỚI THIỆU
Bài toán IM : Tối đa hóa số người theo dõi với một số tiền cho trước
- Tưởng tượng bạn là trưởng phòng truyền thông của Đại học Phenikaa, vàđược giao nhiệm vụ thúc đẩy hình ảnh của trường thông qua một chiến dịch quảng cáo mới Tuy nhiên, ngân sách B mà bạn được cung cấp là có hạn Điều này đặt ra một thách thức lớn: làm thế nào để bạn tối đa hóa phạm vi tiếp cận của chiến dịch đó thông qua mạng lưới các KOL (Key Opinion Leader - Người có ảnh hưởng) mà trường đã xác định trước?
- Mỗi KOL i trong danh sách này sẽ có ảnh hưởng tới một tập hợp S_i những người theo dõi (followers) nhất định, và việc thuê mỗi KOL i sẽ đòi hỏi một khoản chi phí p_i Mục tiêu của bạn là chọn một nhóm KOL sao cho tác động của chiến dịch (số lượng người theo dõi bị ảnh hưởng) được tối đa hóa, với điều kiện tổng chi phí thuê KOL không vượt quá ngân sách hạn chế (sum p_i <= B)
Sơ đồ chi tiết cấu trúc thuật toán :
Trang 4Cấu trúc chung của thuật toán bao gồm 4 phần : Tạo Data , Tạo lời giải đầu, Cải thiện và Kết quả
Trang 5CHƯƠNG 2: PHÂN TÍCH VÀ GIẢI QUYẾT VẤN ĐỀ
A Data
Data được thiết kế phải có những ràng buộc cụ thể Trong dự án này, data được tạo ra bởi các thông số : numFamous(số người nổi tiếng), fund(quỹ được cấp), seedFollower(số follower hạt giống của mỗi người nổi tiếng) , interfer (mật độ giao thoa),partial(tỉ lệ thiên vị số lượng follower), amp(biên độ dao động của giá tiền)
Mã giã của hàm tạo Data :
Trang 6balance = price / tb_price
StandNumFollower = balance * seedFollower
numFollowers = StandNumFollower + random(StandNumFollower * partial) while numFollowers > 0:
print "Cannot write into file!"
Đầu tiên , số quỹ và số người nổi tiếng được khởi tạo ngẫu nhiên trong khoảng mong muốn Sau đó,tính được số tiền trung bình trên mỗi người nổi tiếng() :
tb = fund/numFamous
Số tiền trung bình này sẽ được sử dụng để tạo số tiền cho mỗi Famous
Ban đầu, ta sẽ khởi tạo tỉ lệ mua (tile) Tỉ lệ này tránh việc mua hết Famous mà chưa hết tiền Ví dụ : tỉ lệ là 2, ta chỉ có thể mua được khoảng ½ = 50% người nổi tiếng Số tiền trung bình của mỗi người sẽ được co về cùng với tỉ lệ đã chọn
Trang 7Tb_price = tb/tileTrong bài này , tile dao động từ 2->4.5 (Mỗi file data sẽ có 1 tỉ lệ khác nhau ) Sau
đó, giá tiền của mỗi người sẽ được chọn ngẫu nhiên, nhưng dao động với biên độ
Từ đây, số người theo dõi chuẩn của 1 người được sinh ra :
StandardNumFollower = balance * seedFollwerSau khi có số người theo dõi chuẩn, số người theo dõi của 1 người sẽ được sinh
ra, với thông số thiên vị - partial Nếu partial = 0.3, StandardNumFollower = 100,
Số người theo dõi của famous đó sẽ được dao động trong khoảng 70-130.Tập follower sinh ra sẽ được đặt trong khoảng giao thoa - interfer Điều đó có nghĩa , nếu interfer = 0.6 , thì hai famous sẽ trùng nhau khoảng 60% follower
Để khởi tạo tập follower cho mỗi Famous trong miền giao thoa , ta sẽ dự đoán tổng
số Follower trong toàn bộ Data :
MaxNumFollowers = numFamous * seedFollower * (1 + partial / 2)
Sau đó co lại để tập follower của các Famous có thể giao nhau :
int f = rand() % MaxNumFollowers*(1-interfer/2) + 1;
Ở đây, id của các follower sẽ được sinh ra ngẫu nhiên trong khoảng :
Trang 8vector<int> price; // danh sách giá thuê c a các Famous ủ
vector<vector<int>> followers; // danh sách follower ng v i id c a Famous ứ ớ ủ
};
B Tạo lời giải đầu
Trong bài này, có 5 cách tạo lời giải đầu được tạo ra để so sánh với nhau Tuy nhiên, tổng thể sử dụng Heristic theo 2 cách : Trước khi bắt đầu chọn và trong quá trình chọn Famous
Một Struct Solution được tạo ra để giải quyết bài toán
Các thuộc tính của Solution :
struct Solution {
Input input; // Dữ liệu đầu vào
unordered_set<int> selected; // danh sách Famous đã chọn
unordered_set<int> left; // danh sách Famous chưa chọn
int Used; // số tiền đã dùng
int score; // Tổng số Follower có được
unordered_map<int, int> store; // Danh sách id follower đã chọn và số lần được chọn float ratioH; // tỉ lệ kết hợp Heristic
vector<float> h1; // danh sách Heristic 1 của toàn bộ Famous
vector<float> h2; // danh sách Heristic 2 của toàn bộ Famous
unordered_map<int, unordered_set<int>> data;
print "Done make Solution !"
Constructor của Solution có các chức năng :
- Load data từ file đầu vào : this.input = Input(filename);
- Làm mới lời giải : clearSolution
- Thiết lập danh sách Hersitic của các Famous dưới dạng vector : VecHeristic1() và VecHeristic2()
Trang 9- makeData() : Hàm tạo bản sao của tập input.followers
(vector<vector<>>) về dưới dạng unordered_map<int,unordered_set>> , tăng tốc quá trình thiết lập danh sách Heristic 2
- Data.clear() : giải phóng bộ nhớ , xóa bản sao Data khi không còn giá trị
Trang 10Cả 2 hàm đều thiết lập 1 vector chứa Heristic của lần lượt các Famous : h1
và h2 Cả 2 vector đều được chuẩn hóa về dạng 0->1 và đồng biến với nhau( số càng gần 0 càng tốt, càng gần 1 càng tệ ) .
Trong hàm Vecheristic2() , có sử dụng 2 hàm con lồng nhau :
RepeatVector()<-findPercent() :
function findRepeat(idf): || function findPercent(idf):
for id = 1 to input.numFamous: || percent = (float) repeatCount /
if idf exists in data[id]:
count++
return count
Hàm findRepeat() trả về số idol của 1 follower thông qua id của follower đó bằng cách tìm kiếm chính nó trong tập follower của các Famous khác Việc tìm kiếm dựa trên bản sao Data của tập input.follower giúp giảm độ phức tạp từ O(N.m) xuống O(m) N,m là tổng số follower và tổng số Famous và giảm độ phứctạp của việc thiết lập danh sách Heristic2 từ O(m.N^2) xuống O(m.N)
Hàm findPercent() trả về số idol(của 1 follower)/tổng Famous(của cả data)
Hàm này tạo ra 1 bản sao của input.follower, chuyển về dạng
unordered_map<int,unordered_set<>> Mục đích tạo ra để tăng quá trình thiết lập Heristic2 Bản sao sẽ được xóa ngay sau khi thiết lâp thành công vector Heristic2 tại hàm VecHeristic2() để tiết kiệm bộ nhớ
Ch n ngẫỹu nhiên ọ Kêốt qu đẫầuả
Trang 11for id in range(0, H0.size()):
Kêốt qu ban đẫầu ả
S d ng Heristic(1) ử ụ firstHeristic
O(n)
MasterFirstSolution O(n) Kêốt qu ban đẫầu ả Đánh giá Famous
tr ướ c khi ch n ọ
Trang 12H = empty vector of floats
for i in range(0, input.numFamous):
kq = ratioH * h1[i] + (1 - ratioH) * h2[i]
H.push_back(kq)
return H
Hàm này trả về vector chứa Heristic tổng hợp của Heristic 1&2 theo tỉ lệ ratioH Hai vector h1&h2 chứa 2 Heristic của các Famous đã được khởi tạo trong Constructor của Solution
Sau khi có được vector chứa Heristic tổng hợp của các Famous, ta sẽ tiến hành chọn ra Famous có Heristic tốt nhất : minID = getMinIndex(H) cho tới khi hết quỹ
4 Hàm Training()
Hàm traning nhằm tìm ra tỉ lệ kết hợp tối ưu giữa Heristic1 và Heristic2 function training():
print "Preparing to training "
MasterFirstSolution() // Obtain initial solution
print "Ratio: " + ratioH + "-"
MasterFirstSolution() // Thực hiện Tạo lại lời giải đầu với tỉ lệ mới
newScore = score
print "New score: " + newScore
if newScore >= bestScore:
Trang 13print "Best Score: " + bestScore
print "Best ratioH: " + bestRatioH
this->ratioH = bestRatioH // Cập nhật tỉ lệ ratioH khi tìm được tỉ lệ tối ưu
MasterFirstSolution() // Thực hiện lại lời giải đầu với tỉ lệ tốt nhất
Hàm traning() được thiết kế để tìm kiếm tỉ lệ kết hợp Heristic tối ưu bằngcách thực hiện liên tục lại lời giải đầu (masterFirstSolution()) với tỉ lệ ratioHđược điều chỉnh từng chút một Ban đầu hàm được thiết kế để tự tìm đến tỉ
lệ tối ưu, tuy nhiên còn 1 số lỗi chưa thể khắc phục, hiện giờ hàm sẽ quét
200 tỉ lệ từ 0 đến 1 để tìm tỉ lệ tối ưu nhất
5 Hàm Putting()
Hàm putting() chọn ra famous được đánh giá tốt nhất tại thời điểm chọn, thay vì tất
cả đã được đánh giá ngay từ ban đầu như MasterFirstSolution()
for each famous in RemainingFollower:
for each id in input.followers[maxID - 1]:
famous.second.erase(id)
RemainingFollower.erase(maxID)
print "Done puting with Score: " + score
Kêốt qu ban đẫầu ả tốối u ư Greedỹ Search Putting
O(n /4) 2
Trang 14Nguyên lý :
- Tại thời điểm chọn Famous , xét xem trong danh sách những Famous chưa được chọn, Famous có số Follower được thêm mới(nếu được chọn) / giá thuê là nhỏ nhất sẽ được chọn
- Sau mỗi lần chọn 1 Famous, xóa đi Famous đó tại tập chưa được chọn,
và xóa đi những follower vừa được chọn ở tất cả những Famous còn lại
- 1 unordered_map<int,unordered_set<int>> RemainningFollower sẽ lưu lại tất cả những Famous chưa được chọn và danh sách những Follower chưa được chọn của Famous đó
- Ban đầu khởi tạo tập RemainingFollower = setRemainingFollower();Hàm sử dụng 2 hàm con : setRemainingFollower() và getMaxRemainingID().function setRemainingFollower(a): function getMaxRemainingID(a):
f = empty unordered_set max = -1
for follow in input.followers: for id in a
f.clear() r = float(id.second.size()) / input.price[id.first - 1] for id in follow: if r > max:
f.insert(id) max = r
a[count] = f maxID = id.first
count++ return maxID
Cả 2 hàm sử dụng dầu vào là 1 unordered_map<int,unordered_set<int>> a
- Hàm setRaminingFollower() khởi tạo cho tập RemainningFollower() banđầu chính là bằng tập vector<vector<int>> Followers đầu vào Nó chỉ chuyển từ dạng vector<vector<>> sang dạng
unordered_map<int,unordered_set<>> để tăng tốc độ tính toán
- Hàm getMaxRemainingID() trả về id của Famous tốt nhất :
C Các hàm cải thiện
1 Hàm Swap_1n1()
Độ phức tạp của thuật toán: O(n2)
function Swap_1n1(realback, theBest):
Trang 16print "Best score: " + bestScore
print "Used: " + Used
theBest[score] = selected // Store the best solution in theBest
Thuật toán :
- Hàm nhận 2 tham số đầu vào là int realback và
unordered_map<int,unordered_set<int>> theBest Hai tham số này đượctruyền tham trị từ hàm cha , sẽ được giải thích bên dưới hàm cha
Trang 17- Hàm bao gồm 2 bước độc lập : Swap 1:n và Swap n:1
- Swap1:n :
+ Nguyên lý : Ưu tiên lấy ra 1 Famous có Heristic tổng hợp tệ nhất trong danh sách đã chọn (selected), và thêm vào những Famous có heristic tốt nhấttrong tập chưa chọn (left) cho tới khi sử dụng hết tiền
+ Việc swap thực sự chỉ được thực hiện sau khi kết thúc toàn bộ quá trình trên Một bộ swap sau khi được thực hiện và tính toán khả năng cải thiện, sẽ ngay lập tức được swap ngược lại để khôi phục trạng thái trước khi swap+ Một bộ bestSwap được lưu trữ bộ swap tốt nhất của cả quá trình sẽ được thực hiện sau cùng Cả quá trình thực sự chỉ có 1 lần swap với bộ swap 1:n tốt nhất được ghi nhận
+ Vì maxPrice = 3 minPrice, 1:n có thể là 1:1, 1:2 hoặc 1:3
+ Các bước thực hiện : Ban đầu , tạo ra 2 queue chứa Heristic giảm dần của tập selected và tăng dần của tập left
+ Biến bestScore được tạo ra để lưu trữ giá trị tốt nhất trong quá trình swap Ban đầu, bestScore được khởi tạo bằng score hiện tại
+ Ở vòng while() thứ nhất, duyệt từng id từ đầu tới hết trong queue của tập select (Famous có Heristic tệ nhất bị quét trước)
+ Với mỗi id được chọn ở vòng while thứ nhất , nhấc ID đó ra khỏi tập selected và tạo 1 vòng while con
+ Ở vòng while này cũng duyệt từ đầu tới cuối queue của tập left (Famous
có Heristic tốt nhất được quyét trước)
+ Một vòng while con được tạo ra để chọn và thêm các Famous vào danh sách đã chọn cho tới khi sử dụng hết tiền
+ lưu lại lịch sử swap bằng cách thêm các id được chọn trong vòng while này vào 1 tập unordered_set<int> added
+ tính toán xem bộ swap hiện tại có cải thiện so với bestScore không + nếu có : gán bestScore=score hiện tại; làm mới bộ bestSwap và thêm bộ swap hiện tại vào;
+ Thực hiện swap lại bằng tập added, khôi phục trạng thái ban đầu + làm mới tập added , chuẩn bị cho vòng tiếp theo
+ xóa đi phần tử đầu tiên của queue của tập chưa chọn(left) – lùi lại 1 bước+ Sau khi đã duyệt toàn bộ các bộ id liền nhau của queue left, queue này đã
bị xóa hết và thoát vòng while hiện tại
+ xóa đi phần tử đầu tiên của queue select
+ chuyển sang id tiếp theo trong queue của tập select
Trang 18+ Khôi phục lại queue left sau khi bị xóa hết để thực hiện lại : queueLeft = coppyLeft
+ quá trình quét hoàn thành khi queue của tập select bị xóa hết
Khi này , bộ bestSwap đã lưu lại được bộ swap tối ưu của Solution hiện tại Thực hiện swap cho bộ bestSwap này
- Swap n:1
+ Nguyên lý tương tự như swap 1:n , hàm này ưu tiên swap cho 1 Famous
có heristic tốt nhất trong left với n Famous có heristic tệ nhất bên selected + Được thực hiện ngay sau hàm swap1:n
+ Làm lại các queue và các queue coppy lưu trữ
+ Có một lưu ý : Trong quá trình nhấc các Famous trong queueSelect ra khỏidanh sách chọn cho tới khi quỹ về dưới mức fund(Used < fund) sau khi thêmvào 1 Famous của queueLeft, nếu queueSelect đã trống, sẽ tự động lấy thêm những ID ảo Vì vậy cần lệnh : if(queueSelect.size() < 1) break; Tuy nhiên đối với lệnh này, sẽ sinh ra lỗi dừng vòng while ngay cả khi Used chưa về dưới mức fund Khi đó best Score sẽ được cải thiện( không hợp lệ)+ Để khắc phục lỗi này, bên dưới có thêm code block sử dụng if để kiểm tra :
if(Used > input.fund){ // khắốc ph c bug do l nh break gẫỹ ra ụ ệ
+ Các bước còn lại tương tự như hàm swap1:n
+ Cuối hàm , ta sao chép Solution tốt nhất cùng với số điểm(bestScore) sau toàn bộ quá trình vào tập theBest để truyền ra bên ngoài hàm cha
2 Hàm LocalSearch()
Trang 19Hàm Swap_1n1() chỉ chọn ra và swap cho 1-2 bộ swap tối ưu nhất của Solution dựa trên đánh giá heristic Việc thực hiện vòng lặp liên tục cho hàm này sẽ nhanh chóng đưa kết quả tới maximum.
Tuy nhiên, việc này có 2 nhược điểm :
- Dễ gặp Local Maximum
- Sẽ dừng lại khi không thể cải thiện
Để khắc phục điều này, sử dụng vòng lặp kết hợp với kỹ thuật Simulated anneling tương tụ :
Trang 20+ Hàm sẽ thực hiện vòng lặp cho hàm Swap_1n1
+ hàm swap sẽ liên tục được thực hiện cho tới khi không thể cải thiện + Khi không thể cải thiện => gặp phải local maximum Hàm sẽ tự động kích hoạt Simulated annealing/ bước lùi để trượt xuống khỏi Local maximum Bước lùi
+ Bước lùi : Khi xét điều kiện newScore > bestScore bên trong hàm swap_1n1, giá trị bestScore được phép lùi lại 1 khoảng – realback :
newScore > bestScore – realback;
realback = 0; // chỉ cho phép lùi lần đầu
+ Giá trị bước lùi được giảm dần sau mỗi vòng swap_1n1 , giá trị back được cài đặt tăng dần theo cấp số cộng , do đó giá trị bước lùi càng về cuối càng giảm và giảm nhanh dần về 0
+ Điều này là do càng về cuối, ta càng tiếp cận với kết quả tối ưu, các bước
đi xấu phải càng hạn chế
+ Giá trị bước lùi tối đa ban đầu được cài đặt : sqrt(bestScore/50) Giá trị này được tìm ra trong quá trình chạy thử nhiều giá trị khác nhau Tuy nhiên
nó có thể được tùy chỉnh
+ Trong toàn bộ quá trình xảy ra vòng lặp Swap, một bộ lưu trữ trạng thái lờigiải tốt nhất sẽ được truyền vào bên trong hàm con, lưu lại lời giải tối ưu saumỗi vòng lặp
+ Việc này là do quá trình đi lùi sẽ có thể gây ra nhiễu loạn kết quả, bestScore dao động lên xuống
+ Sau khi kết thúc vòng lặp, bộ lưu trữ sẽ dựa theo giá trị bestScore tốt nhất
để thực hiện lại lời giải tối ưu nhất
3 Hàm LastSolution()
Giá trị bước lùi trong hàm Local Search lớn có thể gây ra kết quả xấu không mong muốn Tuy nhiên nếu giá trị quá nhỏ, có thể dẫn đến việc không đủ năng lượng để bước qua quá nhiều Local Maximum (khi realback tiến về 0)
Khi này, ta thực hiện reset lại 1 phần lời giải bằng hàm reBorn():
function reBorn(p):
numKill = p * selected.size()
select = makeUnorderHeristic(selected)
lef = makeUnorderHeristic(left)