最近在帮一初创app写推荐系统,顺便学习一波用户兴趣高速检索的冠军算法。

写总结前贴出冠军代码的git地址:https://github.com/ChuanyuXue/CIKM-2019-AnalytiCup

该算法分三步:基于Apririo的item_CF、特征提取、排序。

先看第一步,item_CF可以说是很传统的算法了。该步被作者分为七个部分:

1、generate_user_logs:把用户分组,并在每个组中统计用户的行为日志,以方便后续的并行化处理

  1. import multiprocessing as mp
  2. import time
  3. import pandas as pd
  4. import numpy as np
  5.  
  6. def reduce_mem_usage(df):
  7. """ iterate through all the columns of a dataframe and modify the data type
  8. to reduce memory usage.
  9. """
  10. start_mem = df.memory_usage().sum()
  11. print('Memory usage of dataframe is {:.2f} MB'.format(start_mem))
  12.  
  13. for col in df.columns:
  14. col_type = df[col].dtype
  15.  
  16. if col_type != object:
  17. c_min = df[col].min()
  18. c_max = df[col].max()
  19. if str(col_type)[:3] == 'int':
  20. if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
  21. df[col] = df[col].astype(np.int8)
  22. elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
  23. df[col] = df[col].astype(np.int16)
  24. elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
  25. df[col] = df[col].astype(np.int32)
  26. elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
  27. df[col] = df[col].astype(np.int64)
  28. else:
  29. if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
  30. df[col] = df[col].astype(np.float16)
  31. elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
  32. df[col] = df[col].astype(np.float32)
  33. else:
  34. df[col] = df[col].astype(np.float64)
  35. else:
  36. df[col] = df[col].astype('category')
  37.  
  38. end_mem = df.memory_usage().sum()
  39. print('Memory usage after optimization is: {:.2f} MB'.format(end_mem))
  40. print('Decreased by {:.1f}%'.format(100 * (start_mem - end_mem) / start_mem))
  41.  
  42. return df
  43.  
  44. def generate_logs_for_each_group(matrix, q):
  45. user_log = dict()
  46. for row in matrix:
  47. user_log.setdefault(row[0], [])
  48. user_log[row[0]].append(row[1])
  49. print('This batc is finished')
  50. q.put(user_log)

reduce_mem_usage()使用了机器学习中panda输入数据通用的内存优化方法,通过memory_usage().sum()统计矩阵的内存使用量,np.iinfo().min()/max()分别用于找到一个列表中的最小/大数,这里np.iinfo(int8).min()/max()的作用类似于找到该数据类型的最小/大值,比较矩阵中的数据是否处于该区间,若处于,则通过astype()的方法改变数据类型,使其运算占用的内存空间最小;在循环遍历的过程中,要保证数据的类型是由范围由小到大排序的。

generate_logs_for_each_group用于构建用户日记字典,其中setdefault()用于在用户日志字典中添加键与对应的默认值;

主函数:

  1. data = reduce_mem_usage(pd.read_csv(path+'user_behavior.csv', header=None))
  2. user = pd.read_csv(path+'user.csv', header=None)
  3. item = pd.read_csv(path+'item.csv', header=None)
  4.  
  5. data['day'] = data[3] // 86400
  6. data['hour'] = data[3] // 3600 % 24
  7.  
  8. data = data.drop(3, axis=1)
  9.  
  10. data.columns = ['userID','itemID','behavoir','day','hour']
  11. user.columns = ['userID', 'sex', 'age', 'ability']
  12. item.columns = ['itemID', 'category', 'shop', 'band']
  13.  
  14. data = data.drop_duplicates(['userID','itemID'],keep="last")
  15. data = data.sort_values(['day','hour'], ascending=True).reset_index(drop=True)
  16.  
  17. users = list(set(user['userID']))
  18.  
  19. user_groups = [users[i: i + len(users) // CPU_NUMS] for i in range(0, len(users), len(users) // CPU_NUMS)]
  20.  
  21. q = mp.Queue()
  22. for groupID in range(len(user_groups)):
  23. matrix = data[data['userID'].isin(user_groups[groupID])][['userID','itemID']].values
  24. task = mp.Process(target=generate_logs_for_each_group, args=(matrix, q, ))
  25. task.start()
  26.  
  27. start_time = time.time()
  28. print('Waiting for the son processing')
  29. while q.qsize() != len(user_groups):
  30. pass
  31. end_time = time.time()
  32. print("Over, the time cost is:" + str(end_time - start_time))
    for i in range(len(user_groups)):
       temp = q.get()
       f = open('full_logs/userlogs_group' + str(i) + '.txt','w')
       f.write(str(temp))
       f.close()

主函数执行数据的导入与处理。这里要注意的就是整个用户组根据CPU的数量被分为几组,以多进程的方式执行generate_logs_for_each_group,生成行首为userID,后跟itemID的矩阵;

最后将生成的用户日志写入文件。

2、generate_hot_table:生成中间文件hot_map(统计的商品的出现次数)、upwardmap与downward_map(商品id映射到实数集[0, m],其中m代表商品总数)与usersActivity_map(用户活跃度)。

  1. import pandas as pd
  2. import numpy as np
  3.  
  4. # round2 train的路径
  5. path = '../ECommAI_EUIR_round2_train_20190816/'
  6. data = pd.read_csv(path + 'user_behavior.csv',header=None)
  7.  
  8. data.columns = ['userID','itemID','behavior','timestamp']
  9. data['day'] = data['timestamp'] // 86400
  10. data['hour'] = data['timestamp'] // 3600 % 24
  11.  
  12. user_times = data[['itemID','userID']].groupby('userID', as_index=False).count()
  13.  
  14. user_times.columns = ['userID','itemCount']
  15.  
  16. user_times_map = dict(zip(user_times['userID'], user_times['itemCount']))
  17.  
  18. len(user_times_map)
  19.  
  20. f = open('usersActivity_map.txt', 'w')
  21. f.write(str(user_times_map))
  22. f.close()

这段代码使用groupby().count()对数据按userID进行分类,并对每组数目进行统计,统计所得的数标签为'itemCount',随后与userID一起被写入字典,作为用户活跃度map写入文件;

  1. from sklearn import preprocessing
  2. le = preprocessing.LabelEncoder()
  3. item['encoding'] = le.fit_transform(item['itemID'])
  4.  
  5. upward_map = dict(zip(item['itemID'], item['encoding']))
  6. downward_map = dict(zip(item['encoding'], item['itemID']))

在这里preprocessing.LabelEncoder()被用于给商品编号,fit_transform对商品编号序列进行归一化处理,最后生成商品id映射。

  1. temp = data[['itemID','behavior']].groupby('itemID',as_index=False).count()
  2. hot_map = dict(zip(temp['itemID'], temp['behavior']))

groupby().count()用法同上。按商品id分组统计,算出商品出现次数。

文件的最后是保存文件函数,不多做解释。

  1. def save_to_file(trans_map, file_path):
  2. trans_map = str(trans_map)
  3. f = open(file_path, 'w')
  4. f.write(trans_map)
  5. f.close()
  6.  
  7. save_to_file(hot_map,'hot_items_map.txt')
  8.  
  9. save_to_file(upward_map,'upward_map.txt')
  10.  
  11. save_to_file(downward_map,'downward_map.txt')

3、generate_original_matrix:统计每个group中的相似度矩阵。

  1. import pandas as pd
  2. import numpy as np
  3. import sys
  4. from scipy.sparse import lil_matrix
  5. import scipy as scp
  6. import time
  7. %load_ext Cython
  8.  
  9. ITEM_NUM = 4318201
  10.  
  11. def get_logs_from_hardisk(path):
  12. f = open(path, 'r')
  13. a = f.read()
  14. dict_name = eval(a)
  15. f.close()
  16. return dict_name
  17.  
  18. f = open('usersActivity_map.txt', 'r')
  19. m = f.read()
  20. user_times_map = eval(m)
  21. f.close()

导入数据,不多解释。

  1. import datetime
  2. import math
  3.  
  4. cpdef calculate_matrix(mat, list user_logs, dict user_times_map):
  5. cdef int index, i1, i2, count
  6. cdef list item_log
  7. cdef tuple u
  8.  
  9. count = 0
  10. for u in user_logs:
  11. count += 1
  12. if count % 1000 == 0:
  13. print('The %d'%count + ' users are finished.')
  14. print(datetime.datetime.now().strftime('%H:%M:%S'))
  15.  
  16. item_log = u[1]
  17.  
  18. for index, i1 in enumerate(item_log):
  19. for i2 in item_log[(index+1): ]:
  20. weight = 1/(math.log(1+user_times_map[u[0]]))
  21. mat[i1, i2] += weight
  22. mat[i2, i1] += weight
  23. return mat

这部分数据用于计算物品相似度矩阵,在第一个文件生成用户行为日志中,对每一行进行计算,每个用户的item序列对应每行中第二个元素开始的列表;循环遍历物品列表,将物品与所有它之后的物品的相似度加w,w为用户对推荐系统的贡献度,由之前的第二个文件生成的用户活跃度表提供参数,计算公式为:

$w_u=\frac{1}{log(l_u+1)}$

其中l_u为用户活跃度。下面是计算相似度矩阵的主函数:

  1. user_logs = get_logs_from_hardisk('full_logs/userlogs_group0.txt')
  2. f = open('upward_map.txt','r')
  3. upward_map = eval(f.read())
  4. f.close()
  5. for u in user_logs:
  6. user_logs[u] = [int(upward_map[x]) for x in user_logs[u]]
  7. user_logs = list(user_logs.items())
  8.  
  9. for i in range(0, len(user_logs), 10000):
  10. print('The %d '%i + ' batch is started!')
  11. print('--------------------------------')
  12. mat = lil_matrix((ITEM_NUM+1, ITEM_NUM+1), dtype=float)
  13. mat = calculate_matrix(mat, user_logs[i: i + 10000], user_times_map)
  14. scp.sparse.save_npz('tmpData/sparse_matrix_%d_batch_group0.npz'%i, mat.tocsr())
  15. print('save successfully')
  16. print('--------------------------------')

调入用户日志,调入itemID-实数的文件(用于规范矩阵、减小矩阵规模),将用户日志中的itemID用1~m的实数代替;使用lil_matrix()新建矩阵,分组计算物品相似度,并将相似度矩阵文件保存,第三步操作到此结束。

4、Merge:将多个group中的相似性矩阵合并。

  1. import pandas as pd
  2. import numpy as np
  3. from scipy.sparse import *
  4. import scipy
  5. import os
  6.  
  7. path = 'tmpData/'
  8.  
  9. lenth = 359850
  10. for i in range(0, 12):
  11. mat = None
  12. start = lenth * i
  13. end = lenth * (i + 1)
  14. count = 0
  15. for name in os.listdir('tmpData/'):
  16. if name[-3:] == 'npz':
  17. if mat == None:
  18. mat = load_npz(path + name)[start: end]
  19. else:
  20. mat += load_npz(path + name)[start: end]
  21. count += 1
  22. scipy.sparse.save_npz('commonMatrix/common_matrix_from_%d_to_%d.npz'%(start, end), mat)
  23. print('save success for %d batch'%i)
  24. print('-------------------------')

功能官方文档写的很明显了。主要思想就是分段合并相加相似度矩阵。

5、Save_sparse_to_dense:将相似性矩阵转化为可供快速检索的哈希结构。

  1. import numpy as np
  2. from scipy.sparse import *
  3. import os
  4.  
  5. os.listdir('commonMatrix/')
  6.  
  7. for name in os.listdir('commonMatrix/'):
  8. mat = load_npz('commonMatrix/' + name).tolil()
  9. l = []
  10. for i in range(mat.shape[0]):
  11. _, a, b = find(mat[i])
  12. index = np.where(b > 1.5)
  13. #l.append(sorted(list(zip(a[index], b[index])),key= lambda x:x[1], reverse=True))
  14.  
  15. c = np.array( [round(x,3) for x in b] )
  16. l.append(sorted(list(zip(a[index], c[index])),key= lambda x:x[1], reverse=True))
  17.  
  18. l = str(l)
  19. f = open('common_dense_valued_small/' + name, 'w')
  20. f.write(l)
  21. f.close()
  22. print('finished')

这段矩阵转哈希表程序首先读入相似度矩阵,对矩阵的每一行进行处理,使用mat()将数组转换成矩阵,使用find()找到非零元素的下标并储存值,下一步将非零元素中大于1.5的元素排序(我也不知道为什么要取1.5 可能是为了降低运算量而作的剪枝),地址和数值被加入表项中。

6、6_Sta_for_SparseMatrix:用于对相似度的改进

  1. import pandas as pd
  2. import numpy as np
  3. import time
  4. from scipy.sparse import *
  5. import os
  6. import re
  7.  
  8. ## 统计每个商品的打分次数(用train)
  9. f = open('hot_items_map.txt', 'r')
  10. rating_times_map = eval(f.read())
  11. f.close()
  12.  
  13. item_dict = {}
  14.  
  15. for name in os.listdir('common_dense_valued_small/'):
  16. start_time = time.time()
  17. f = open('common_dense_valued_small/' + name, 'r')
  18. l = f.read()
  19. l = eval(l)
  20. f.close()
  21. end_time = time.time()
  22. print('load file: %d sec'%((end_time - start_time)))
  23.  
  24. name = re.findall(r'\d+', name)
  25. start = int(name[0])
  26. end = int(name[1])
  27.  
  28. start_time = time.time()
  29.  
  30. for i in range(start, end):
  31. tmp_list = []
  32. [tmp_list.append( (x[0], round(x[1] / rating_times_map[i], 4) ) ) for x in l[i - start] if x[0] != i]
  33. if len(tmp_list) > 0:
  34. item_dict[i] = sorted(tmp_list,key=lambda x:x[1], reverse=True)[:500]
  35.  
  36. end_time = time.time()
  37. print('This batch is finished, time cost: %d sec'%((end_time - start_time)))

这个文件涉及正则表达式和上一个文件生成的哈希表,首先读入之前生成的商品热度表与哈希表,通过正则表达式findall()读出文件名中的开始item序号与结束item序号(文件命名按分组区间,如:'common_matrix_from_3598500_to_3958350.npz')遍历这个区间内item的哈希表,每个item i、j之间的相似度都除以i的商品热度,作为热度过高的商品的惩罚(若一件商品很热门,则不管用户喜不喜欢,用户对其作出行为的可能性都很大,极有可能出现在不同用户的行为列表中,这样会使得相似度偏大)。将惩罚处理过的哈希表装入item_dict中。

最后,将生成的item_dict写入文件。

  1. f = open('item_Apriori.txt','w')
  2. f.write(str(item_dict))
  3. f.close()

7、generate_recall:官方没有写特别说明,个人理解为这是最后的数据召回。

  1. import pandas as pd
  2. import numpy as np
  3.  
  4. def load_data(path):
  5. user = pd.read_csv(path + 'user.csv',header=None)
  6. item = pd.read_csv(path + 'item.csv',header=None)
  7. data = pd.read_csv(path + 'user_behavior.csv',header=None)
  8.  
  9. data.columns = ['userID','itemID','behavior','timestamp']
  10. data['day'] = data['timestamp'] // 86400
  11. data['hour'] = data['timestamp'] // 3600 % 24
  12.  
  13. ## 生成behavior的加权
  14. data['day_hour'] = data['day'] + data['hour'] / float(24)
  15. data.loc[data['behavior']=='pv','behavior'] = 1
  16. data.loc[data['behavior']=='fav','behavior'] = 2
  17. data.loc[data['behavior']=='cart','behavior'] = 3
  18. data.loc[data['behavior']=='buy','behavior'] = 1
  19. max_day = max(data['day'])
  20. min_day = min(data['day'])
  21. data['behavior'] = (1 - (max_day-data['day_hour']+2)/(max_day-min_day+2)) * data['behavior']
  22.  
  23. item.columns = ['itemID','category','shop','brand']
  24. user.columns = ['userID','sex','age','ability']
  25.  
  26. data = pd.merge(left=data, right=item, on='itemID',how='left')
  27. data = pd.merge(left=data, right=user, on='userID',how='left')
  28.  
  29. return user, item, data

首先还是熟悉的数据导入操作,与前面不同的是这里加入了用户行为权值,通过.loc方法将数值赋值到数据矩阵相应的行。最后使用merge()对用户行为数据、用户数据与商品数据进行拼接,这样一条信息就包含用户(ID、性别、年龄etc)、商品(品牌、价格、店家etc)、行为信息了。

  1. def get_recall_list(train, targetDay, k=300):
  2. train_logs = dict()
  3.  
  4. if targetDay > max(train['day']):
  5. for row in train[['userID','itemID','behavior']].values:
  6. train_logs.setdefault(row[0], dict())
  7. if row[1] in upward_map:
  8. train_logs[row[0]].setdefault(upward_map[row[1]],0)
  9. train_logs[row[0]][upward_map[row[1]]] = max(train_logs[row[0]][upward_map[row[1]]],row[2])
  10. else:
  11. user_List_test = set(train.loc[train['day']==targetDay,'userID'])
  12. train = train[train['day'] < targetDay]
  13.  
  14. for row in train[['userID','itemID','behavior']].values:
  15. if row[0] in user_List_test:
  16. train_logs.setdefault(row[0], dict())
  17. if row[1] in upward_map:
  18. train_logs[row[0]].setdefault(upward_map[row[1]],0)
  19. train_logs[row[0]][upward_map[row[1]]] = max(train_logs[row[0]][upward_map[row[1]]],row[2])
  20.  
  21. for each_user in train_logs:
  22. sum_value = sum(train_logs[each_user].values())
  23. if sum_value > 0:
  24. for each_item in train_logs[each_user]:
  25. train_logs[each_user][each_item] /= sum_value
  26.  
  27. result_logs = dict()
  28. for u in train_logs:
  29. result_logs.setdefault(u, list())
  30. for i in set(train_logs[u].keys()):
  31. if i in item_dict:
  32. tmp_list = [ (x[0], train_logs[u][i]*x[1]) for x in item_dict[i]]
  33. result_logs[u] += tmp_list
  34.  
  35. for u in result_logs:
  36. result_logs[u] = get_unique_inorder([(downward_map[x[0]], x[1]) for x in sorted(result_logs[u], key=lambda x:x[1], reverse=True)
  37. if x[0] not in train_logs[u]], k=300)
  38.  
  39. return result_logs

这部分可以说是代码中的核心部分了,倘若要计算的日期大于最大时间戳,则在训练文件中加入用户ID-空字典的键值对,如果该条信息中用户作出行为,则在itemID-编号字典中找出编号作为用户ID-dict()中,字典的键,之前的行为权值取最大值作为值加入。

若预测天数小于最大时间戳,则取数据矩阵中天数是目标天数以前的数据,取目标天数数据作为测试集(代码示例中并未用到);之后所做的与预测大于最大时间戳的操作相同。

对于训练集[(uesrID,itemID, value),……]中的每个user,首先将其键下所有的值相加,在用各个值除以相加所得的和,这步起到数据归一化的作用。

然后是生成对于每个用户物品最终分值的一步,每个用户所对应的每批产生过行为的商品,将归一化的行为权值乘以矩阵中每个与每个物品的相似度,所得结果就是对这个用户而言,每个商品的兴趣度,以[用户,[(商品,兴趣度)……]]排列。

接着将生成的序列降序排序,同时调用downward_map将编号映射回itemID,使用get_unique_inorder()去重,最终result_logs就是对每个用户来说兴趣度最大的300个商品列表矩阵。

最后是几个格式变换函数:

  1. def generate_pairs(recall):
  2. result = []
  3. for u in recall:
  4. for i in recall[u]:
  5. result.append([u,i[0],i[1]])
  6. return result
  7.  
  8. def reshape_recall_to_dataframe(recall):
  9. result = generate_pairs(recall)
  10. result = pd.DataFrame(result)
  11. result.columns = ['userID','itemID','apriori']
  12. return result

在主函数中,召回矩阵首先从[ [用户,[(商品,兴趣度)……] ] ……]的格式转换为[ [用户,商品,兴趣度]……],再被加注标签,最后写入csv文件:

  1. #path = './'
  2. path = '../ECommAI_EUIR_round2_train_20190816/'
  3.  
  4. ## The target date(16 means online, 15 means underline test, 14 means underline train)
  5. targetday = 15
  6.  
  7. ## The lenth of recall list, the default is 300
  8. lenth = 300
  9.  
  10. ## The name of generated recall file
  11.  
  12. name = 'recall_list_round2_%dday_%dlenth.csv'%(targetday, lenth)
  13.  
  14. user, item, data = load_data(path = path)
  15.  
  16. #tempory_path = './tempory_file/'
  17. tempory_path = './'
  18. f = open(tempory_path + 'upward_map.txt','r')
  19.  
  20. upward_map = f.read()
  21. upward_map = eval(upward_map)
  22. f.close()
  23.  
  24. f = open(tempory_path + 'downward_map.txt','r')
  25. downward_map = f.read()
  26. downward_map = eval(downward_map)
  27. f.close()
  28.  
  29. f = open(tempory_path + 'item_Apriori.txt','r')
  30. tmp = f.read()
  31. item_dict = eval(tmp)
  32. f.close()
  33.  
  34. recall_logs = get_recall_list(data, targetDay=targetday, k=lenth)
  35.  
  36. recall_df = reshape_recall_to_dataframe(recall_logs)
  37.  
  38. temp = pd.merge(left=recall_df, right=data[data['day'] == targetday][['userID','itemID','behavior']],
  39. on=['userID','itemID'], how='left').rename(columns={'behavior':'label'})
  40.  
  41. len(set(recall_df['userID']) & set(data[data['day'] == targetday]['userID']))
  42.  
  43. len(set(recall_df['userID']))
  44.  
  45. recall_df.to_csv(name, index=False)

其中还穿插着一些行为引入的操作;至此,基于apriori的itemCF源码分析就结束了。

给我的感受是整个itemCF中数学统计的原理非常简单,代码的精巧体现在对多维度数据的处理和利用上。

推荐算法_CIKM-2019-AnalytiCup 冠军源码解读的更多相关文章

  1. 推荐算法_CIKM-2019-AnalytiCup 冠军源码解读_2

    最近在为机器学习结合推荐算法的优化方法和数据来源想办法.抱着学习的态度继续解读19-AnalytiCup的冠军源码. 第一部分itemcf解读的连接:https://www.cnblogs.com/m ...

  2. 量化交易中VWAP/TWAP算法的基本原理和简单源码实现(C++和python)(转)

    量化交易中VWAP/TWAP算法的基本原理和简单源码实现(C++和python) 原文地址:http://blog.csdn.net/u012234115/article/details/728300 ...

  3. [算法1-排序](.NET源码学习)& LINQ & Lambda

    [算法1-排序](.NET源码学习)& LINQ & Lambda 说起排序算法,在日常实际开发中我们基本不在意这些事情,有API不用不是没事找事嘛.但必要的基础还是需要了解掌握. 排 ...

  4. 为什么不推荐Python初学者直接看项目源码

    无论是有没有其他语言的经验,入门Python都很简单.Python拥有简单直观的语法,方便的语法糖,以及丰富的第三方库.只要一个基础的Python教程,大家基本上都能无障碍的入门.在入门之后,很多人对 ...

  5. AFNetworking 3.0 源码解读 总结(干货)(上)

    养成记笔记的习惯,对于一个软件工程师来说,我觉得很重要.记得在知乎上看到过一个问题,说是人类最大的缺点是什么?我个人觉得记忆算是一个缺点.它就像时间一样,会自己消散. 前言 终于写完了 AFNetwo ...

  6. AFNetworking 3.0 源码解读 总结

    终于写完了 AFNetworking 的源码解读.这一过程耗时数天.当我回过头又重头到尾的读了一篇,又有所收获.不禁让我想起了当初上学时的种种情景.我们应该对知识进行反复的记忆和理解.下边是我总结的 ...

  7. Restful 1 -- REST、DRF(View源码解读、APIView源码解读)及框架实现

    一.REST 1.什么是编程? 数据结构和算法的结合 2.什么是REST? - url用来唯一定位资源,http请求方式来区分用户行为 首先回顾我们曾经做过的图书管理系统,我们是这样设计url的,如下 ...

  8. AFNetworking 3.0 源码解读(十一)之 UIButton/UIProgressView/UIWebView + AFNetworking

    AFNetworking的源码解读马上就结束了,这一篇应该算是倒数第二篇,下一篇会是对AFNetworking中的技术点进行总结. 前言 上一篇我们总结了 UIActivityIndicatorVie ...

  9. AFNetworking 3.0 源码解读(十)之 UIActivityIndicatorView/UIRefreshControl/UIImageView + AFNetworking

    我们应该看到过很多类似这样的例子:某个控件拥有加载网络图片的能力.但这究竟是怎么做到的呢?看完这篇文章就明白了. 前言 这篇我们会介绍 AFNetworking 中的3个UIKit中的分类.UIAct ...

随机推荐

  1. c++ 启发式搜索解决八数码问题

    本文对八数码问题 启发式搜索 (C++)做了一点点修改 //fn=gn+hn #include<iostream> #include<queue> #include<st ...

  2. 【Java】抽象类、接口

    什么是抽象类? 特点: - 抽象类几乎普通类一样,除了不能实例化 - 不能实例化不代表没有构造器,依然可以声明构造器,便于子类实例化调用 - 具有抽象方法的类,一定是抽象类 abstract 抽象的 ...

  3. L - Neko does Maths CodeForces - 1152C 数论(gcd)

    题目大意:输入两个数 a,b,输出一个k使得lcm(a+k,b+k)尽可能的小,如果有多个K,输出最小的. 题解: 假设gcd(a+k,b+k)=z; 那么(a+k)%z=(b+k)%z=0. a%z ...

  4. 【论文研读】强化学习入门之DQN

    最近在学习斯坦福2017年秋季学期的<强化学习>课程,感兴趣的同学可以follow一下,Sergey大神的,有英文字幕,语速有点快,适合有一些基础的入门生. 今天主要总结上午看的有关DQN ...

  5. 6. webRTC

    webrtc网上封装的很多,demo很多都是一个页面里实现的,今天实现了个完整的 , A 发视频给 B. 1.) A 方 <!DOCTYPE html> <html id=" ...

  6. deepin15.11小毛病解决

    目录 边缘花屏问题 QQ`Tim头像问题 ssh卡死问题 看直播卡 边缘花屏问题 sudo apt install systemsettings 打开kde系统设置 打开显示与设置,修改如图下,基本上 ...

  7. 算法笔记刷题2(codeup 1928)

    又磕了一晚上,多点测试真的很烦 ,完全不知道错哪里,后来发现是我变量名命名不规范导致自己晕了填错了,其实思路还是对的 我觉得书上的做法也还行,但我不太喜欢用二维数组,所以拿以前写的算天数的程序改装了一 ...

  8. 算法笔记刷题1(codeup 1934)

    准备6月份的拼题甲级中(本来现在这两天就考试了,但是因为疫情的原因延期了) 刚刚开始按算法笔记刷题,今天是探索codeup的第一天. 一开始并没有把多点测试当回事,直到一错再错,心态爆炸... 附上我 ...

  9. Oracle 11g 精简客户端

    通常开发人员会装上一个 oracle客户端,但一般不会在自己的机器上安装Oracle database Oracle 客户端安装体积很大,但是装上去了基本上就用2个功能:TNS配置服务名和sqlplu ...

  10. JVM原理与深度调优(一)

    什么是jvm jvm是java虚拟机 运行在用户态.通过应用程序实现java代码跨平台.与平台无关.实际上是"一次编译,到处执行" 1.从微观来说编译出来的是字节码!去到哪个平台都 ...