当当当当~上一次发博客好像是在去年11月了。歇了这么久了,就趁这个风和日丽的下午,选在数字图像处理课上跟大家分享一下一下上学期最后的数据挖掘课程设计吧!(反正数字图像处理课我是听不懂~)
项目要求用Django开发前端可视化,前端可视化模块是我的组长负责的,所以本博客主要专注于推吉安算法模块的实现、代码讲解。如有问题欢迎与我交流。
1. 项目介绍
音乐推荐系统
必须实现的需求:用户注册/登录/退出,存储音乐数据,存储用户和歌手数据,计算音乐模型,根据基于内容的推荐算法和基于物品和用户的协同过滤算法等计算用户可能感兴趣的音乐,展示用户可能喜欢的音乐,具有选择用户,选择标签(可选歌曲、歌手等),为你推荐,歌单与歌手推荐,歌单详情与歌单详情页推荐,排行榜,我的足迹,后台管理等功能。
2. 项目演示
网站首页
登陆页面
所有歌曲页面
“猜你喜欢”页面
“我的信息”页面
“浏览记录”页面
3. 模块分解
要开发搭建一个具有推荐功能的音乐网站,工程量着实不小。本次课程设计我们是分组每组三人进行的,三人之间的分工大概是这样的:
4. 数据库中的数据
我们选取了数据集中听歌次数最多的前500名用户,处理得到了这些用户听过的歌,共有25928首
数据处理过后的干净数据是长介个样子滴
【1】歌曲表(⭐歌曲热榜、全部歌曲模块所使用的表)
song_id | title | release | artist_name | year | play_count |
SOBONKR12A58A7A7E0 | You're The One | If There Was A Way | Dwight Yoakam | 1990 | 726885 |
SOAUWYT12A81C206F1 | Undo | Vespertine Live | Bj?rk | 2001 | 648239 |
SOSXLTC12AF72A7F54 | Revelry | Only By The Night | Kings Of Leon | 2008 | 527893 |
SOFRQTD12A81C233C0 | Sehr kosmisch | Musik von Harmonia | Harmonia | 1999 | 425463 |
SOEGIYH12A6D4FC0E3 | Horn Concerto No. 4 in E flat K495: II. Romance (Andante cantabile) | Mozart - Eine kleine Nachtmusik | Barry Tuckwell/Academy of St Martin-in-the-Fields/Sir Neville Marriner | 1999 | 389880 |
SOAXGDH12A8C13F8A1 | Dog Days Are Over (Radio Edit) | Now That's What I Call Music! 75 | Florence + The Machine | 1999 | 356533 |
.... | .... | .... | .... | .... | .... |
【2】歌手表(⭐歌曲热榜所使用的表)
artist_name |
Linkin Park |
Blackmore's Night |
Vanessa Carlton |
Tarkan |
Stelios Kazantzides |
Akon |
Dierks Bentley |
Ry Cooder |
...... |
【3】用户表(⭐登录、注册模块所使用的表)
user_id | username | password | sex | age |
093cb74eb3c517c5179ae24caf0ebec51b24d2a2 | 1 | 123456 | 男 | 17 |
d13609d62db6df876d3cc388225478618bb7b912 | 2 | 123456 | 男 | 21 |
4cbca37009400bb5676ba54c2a4cc24ff0531cb7 | 3 | 123456 | 男 | 24 |
d5d1fc74f29ef42eefc2acf4c8c59da2994a0a16 | 4 | 123456 | 男 | 19 |
81193c20d49bfbca4a72eaa1119f6bb2fc4d9e48 | 5 | 123456 | 女 | 17 |
d19e8c59d207bf3e3c7ca0248a1fa684c00d006b | 6 | 123456 | 男 | 33 |
bedad07238f60df86cb77b2d4ef1441c8f0b3a3b | 7 | 123456 | 男 | 31 |
9c6ebf1d5ba38bcb577149b22e19448f655f6252 | 8 | 123456 | 女 | 25 |
1fc2a7f42424249718cc544a0a1036a69d5bc7b8 | 9 | 123456 | 女 | 15 |
fc05f377863a77d7784b02de2cc06cdecb85968b | 10 | 123456 | 女 | 27 |
.... | .... | .... | .... | ... |
【4】年份表(⭐歌曲热榜所使用的表)
year |
2007 |
2006 |
2002 |
1999 |
2004 |
2003 |
..... |
【5】用户-歌曲表(⭐推荐算法模块所使用的表)
song_id | user_id | listen_count | title | play_count | release | artist_name | year | fractional_listen_count |
SOABHYV12A6D4F6D0F | 093cb74eb3c517c5179ae24caf0ebec51b24d2a2 | 11 | Shadow Of The Day (Album Version) | 13132 | Minutes To Midnight | Linkin Park | 2007 | 0.000837648 |
SOABHYV12A6D4F6D0F | d13609d62db6df876d3cc388225478618bb7b912 | 1 | Shadow Of The Day (Album Version) | 3657 | Minutes To Midnight | Linkin Park | 2007 | 0.000273448 |
SOABHYV12A6D4F6D0F | 4cbca37009400bb5676ba54c2a4cc24ff0531cb7 | 33 | Shadow Of The Day (Album Version) | 3470 | Minutes To Midnight | Linkin Park | 2007 | 0.009510086 |
... | ... | ... | ... | ... | ... | ... | ... | ... |
【6】在Django框架的models中建好表,把他们分别导进去就可以啦!
5. 我所负责的模块
我负责完成的是推荐算法的实现模块,实现了以下几种推荐算法:
UserCF(基于用户的协同过滤)
ItemCF(基于物品的协同过滤)
SVD分解
冷启动(对新注册的用户进行音乐的推荐)
以下是我负责的推荐算法模块的代码
-
# Thanks to Siraj Raval for this module
-
'''两种推荐算法:基于物品相似度和对于新用户的冷启动'''
-
import pandas
as pd
-
import numpy
as np
-
from scipy.sparse
import coo_matrix
-
import math
as mt
-
from scipy.sparse.linalg
import *
#used for matrix multiplication
-
from scipy.sparse.linalg
import svds
-
from scipy.sparse
import csc_matrix
-
-
#Class for Popularity based Recommender System model
-
#计算统计返回歌曲表中每首歌的被听次数,用于新用户(解决冷启动)
-
class popularity_recommender_py():
-
def __init__(self):
-
self.train_data =
None
-
self.user_id =
None
-
self.item_id =
None
-
self.popularity_recommendations =
None
-
-
def create(self, train_data, user_id, item_id):
-
self.train_data = train_data
-
self.user_id = user_id
-
self.item_id = item_id
#进行分类的指标:可以选择 歌曲名 歌手 年份 等等
-
train_data_grouped = train_data.groupby([self.item_id]).agg({self.user_id:
'count'}).reset_index()
#计算按分类指标的播放次数
-
train_data_grouped.rename(columns = {self.user_id:
'score'},inplace=
True)
-
train_data_sort = train_data_grouped.sort_values([
'score', self.item_id], ascending = [
0,
1])
#对计算结果进行排序
-
train_data_sort[
'Rank'] = train_data_sort[
'score'].rank(ascending=
0, method=
'first')
-
self.popularity_recommendations = train_data_sort.head(
10)
#返回前十名推荐歌曲作为推荐结果
-
-
def recommend(self, user_id):
-
user_recommendations = self.popularity_recommendations
-
user_recommendations[
'user_id'] = user_id
-
cols = user_recommendations.columns.tolist()
-
cols = cols[
-1:] + cols[:
-1]
-
user_recommendations = user_recommendations[cols]
-
return user_recommendations
-
-
#基于用户的歌曲相似度进行推荐(基于用户的协同过滤算法)返回的是推荐歌曲的dataFrame
-
class user_similarity_recommender_py():
-
-
def __init__(self):
-
self.train_data =
None
-
self.user_id =
None
-
self.user_title =
None
#user_id
-
self.item_title =
None
-
self.ans_user_set = {}
-
-
def create(self,train_data,user_title,item_title, user_id):
#构造对象
-
self.user_title = user_title
-
self.train_data = train_data
-
self.item_title = item_title
-
self.user_id = user_id
-
-
def get_all_user_train_data(self):
#获取training data中的所有用户id
-
all_users = list(self.train_data[self.user_title].unique())
-
return all_users
-
-
def get_user_items(self, user):
# 获取user用户听过的所有歌的列表
-
user_data = self.train_data[self.train_data[self.user_title] == user]
# user这个用户都听了哪几首歌
-
user_items = list(user_data[self.item_title].unique())
-
return user_items
-
-
def get_similarity_users(self):
#获取前十个相似的用户
-
all_users = self.get_all_user_train_data()
#得到所有用户
-
song_user = set(self.get_user_items(self.user_id))
#得到待推荐用户听过的歌
-
lis = []
-
for i
in all_users:
#对于数据集中的每一个用户
-
l = []
-
song_i = set(self.get_user_items(i))
-
songs_intersection = song_i.intersection(song_user)
#计算这两名用户听过歌曲的交集
-
if len(songs_intersection) !=
0:
-
songs_union = song_i.union(song_user)
#计算这两名用户听过歌曲的并集
-
score = float(len(songs_intersection))/float(len(songs_union))
#计算这两名用户的相似度
-
l.append(i)
-
l.append(score)
-
lis.append(l)
-
lis.sort(key=
lambda x : x[
1], reverse=
True)
#按相似度对用户进行排序
-
ans = []
-
return lis
-
for i
in range(
0, min(
5,len(lis))):
-
ans.append(lis[i][
0])
#得到前五名用户的user_id
-
return ans
-
-
def get_ans(self):
-
ini_songs = self.get_user_items(self.user_id)
-
users = self.get_similarity_users()
#得到与被推荐用户最相似的前五名用户的id
-
columns = [
'similary_users',
'songs']
-
ans = []
-
for i
in users:
#对于每一个推荐结果用户
-
recommend_user = i[
0]
-
songs = self.get_user_items(recommend_user)
#得到推荐结果用户听过的所有歌
-
for recommend_song
in songs:
-
if recommend_song
not
in ini_songs
and recommend_song
not
in ans:
#如果这首歌没被推荐过,待推荐用户也没有听过
-
ans.append(recommend_song)
-
if len(ans) ==
10:
#限制返回的歌曲数量
-
return ans
-
-
#Class for Item similarity based Recommender System model
-
#基于歌曲的受众相似度进行推荐(基于物品的协同过滤算法)返回的是推荐歌曲的dataFrame
-
class item_similarity_recommender_py():
-
def __init__(self):
-
self.train_data =
None
-
self.unique_song_title =
None
-
self.user_id =
None
-
self.item_id =
None
-
self.cooccurence_matrix =
None
-
self.songs_dict =
None
-
self.rev_songs_dict =
None
-
self.item_similarity_recommendations =
None
-
self.dic = {}
-
-
def create(self, train_data, user_id, item_id, unique_song_title):
-
self.train_data = train_data
-
self.user_id = user_id
-
self.item_id = item_id
-
self.unique_song_title = unique_song_title
-
self.get_my_dic()
-
-
def get_user_items(self, user):
-
user_data = self.train_data[self.train_data[self.user_id] == user]
#user这个用户都听了哪几首歌
-
user_items = list(user_data[self.item_id].unique())
-
return user_items
-
-
def get_item_users(self, item):
-
item_data = self.train_data[self.train_data[self.item_id] == item]
-
item_users = set(item_data[self.user_id].unique())
-
return item_users
-
-
def get_all_items_train_data(self):
-
all_items = list(self.train_data[self.item_id].unique())
-
# print("all_items:",all_items)
-
return all_items
-
-
def get_my_dic(self):
-
user_id = self.train_data[
'user_id']
-
song_title = self.train_data[
'title']
-
for i
in song_title:
-
self.dic[i] = []
-
lis = []
-
j =
0
-
for i
in song_title:
-
self.dic[i].append(user_id[j])
-
j +=
1
-
-
def construct_cooccurence_matrix(self, user_songs, all_songs):
#构造相似度矩阵
-
cooccurence_matrix = np.matrix(np.zeros(shape=(len(user_songs), len(all_songs))), float)
-
for i
in range(
0, len(all_songs)):
#遍历所有的歌
-
users_i = set(self.dic[all_songs[i]])
#取出听过这一首歌的用户
-
for j
in range(
0, len(user_songs)):
#遍历待推荐用户听过的所有歌
-
users_j = set(self.dic[user_songs[j]])
#取出听过这一首歌的用户
-
users_intersection = users_i.intersection(users_j)
-
if len(users_intersection) !=
0:
-
users_union = users_i.union(users_j)
-
cooccurence_matrix[j, i] = float(len(users_intersection)) / float(len(users_union))
#用Jacccard相似系数作为衡量相似度
-
else:
-
cooccurence_matrix[j, i] =
0
-
return cooccurence_matrix
-
-
-
def generate_top_recommendations(self, user, cooccurence_matrix, all_songs, user_songs):
-
user_sim_scores = cooccurence_matrix.sum(axis=
0)/float(cooccurence_matrix.shape[
0])
-
#每首歌总得分为这首歌与user_songs中所有歌相似度的平均值
-
user_sim_scores = np.array(user_sim_scores)[
0].tolist()
#得到每首歌的推荐下标
-
sort_index = sorted(((e,i)
for i,e
in enumerate(list(user_sim_scores))), reverse=
True)
#将歌曲按总相似度进行排序
-
columns = [
'user_id',
'song',
'score',
'rank']
-
df = pd.DataFrame(columns=columns)
#构造一个DataFrame类型暂时存储推荐结果
-
rank =
1
#限制返回的歌曲数量
-
for i
in range(
0,len(sort_index)):
#遍历推荐列表
-
if ~np.isnan(sort_index[i][
0])
and all_songs[sort_index[i][
1]]
not
in user_songs
and rank <=
10:
-
#如果这首歌待推荐用户没有听过,并且推荐下标在10以内
-
df.loc[len(df)]=[user,all_songs[sort_index[i][
1]],sort_index[i][
0],rank]
#将这首歌的信息存入df
-
rank = rank+
1
-
ans = list(df[
'song'])
#返回推荐的歌曲名列表
-
return ans
-
-
def recommend(self, user):
-
user_songs = self.get_user_items(user)
#得到待推荐用户听过的所有的歌
-
all_songs = self.unique_song_title
#得到数据集中所有的歌名
-
cooccurence_matrix = self.construct_cooccurence_matrix(user_songs, all_songs)
#计算得到相似度矩阵
-
recommendations = self.generate_top_recommendations(user, cooccurence_matrix,all_songs, user_songs)
#得到推荐的歌曲名列表
-
return recommendations
-
-
-
#SVD分解预测用户的评分 返回值类型还需处理
-
class SVD():
-
def __init__(self):
-
self.User_id =
None
-
self.uTest =
None
-
self.train_data =
None
-
self.user_id =
None
-
self.mt_candidate =
None
-
self.small_set =
None
-
self.K =
None
-
self.MAX_UID =
None
-
self.MAX_PID =
None
-
self.urm =
None
-
-
def create(self,train_data, user_id):
-
self.train_data = train_data
-
self.User_id = user_id
-
# self.user_id = user_id
-
-
def process_data(self):
-
self.small_set = self.train_data
-
user_codes = self.small_set.user_id.drop_duplicates().reset_index()
# 得到所有的用户,去除重复值
-
song_codes = self.small_set.song_id.drop_duplicates().reset_index()
# 得到所有的歌id,去除重复值
-
user_codes.rename(columns={
'index':
'user_index'}, inplace=
True)
-
song_codes.rename(columns={
'index':
'song_index'}, inplace=
True)
-
song_codes[
'so_index_value'] = list(song_codes.index)
-
user_codes[
'us_index_value'] = list(user_codes.index)
-
self.small_set = pd.merge(self.small_set, song_codes, how=
'left')
-
self.small_set = pd.merge(self.small_set, user_codes, how=
'left')
-
us_ids = user_codes[
'user_id']
-
us_index_values = user_codes[
'us_index_value']
-
for i
in range(len(us_ids)):
-
if us_ids[i] == self.User_id:
-
self.uTest = us_index_values[i]
-
break
-
if self.uTest !=
None:
-
break
-
self.mat_candidate = self.small_set[[
'us_index_value',
'so_index_value',
'fractional_play_count']]
-
-
def get_data_sparse(self):
-
data_array = self.mat_candidate.fractional_play_count.values
-
row_array = self.mat_candidate.us_index_value.values
-
col_array = self.mat_candidate.so_index_value.values
-
-
data_sparse = coo_matrix((data_array, (row_array, col_array)), dtype=float)
-
self.K =
500
-
self.urm = data_sparse
-
self.MAX_PID = self.urm.shape[
1]
-
self.MAX_UID = self.urm.shape[
0]
-
-
def compute_svd(self, urm, K):
#进行矩阵分解,返回U、S、Vt三个矩阵
-
U, s, Vt = svds(urm, K)
-
dim = (len(s), len(s))
-
S = np.zeros(dim, dtype=np.float32)
-
for i
in range(
0, len(s)):
-
S[i, i] = mt.sqrt(s[i])
-
U = csc_matrix(U, dtype=np.float32)
#用来进行稀疏矩阵的压缩
-
S = csc_matrix(S, dtype=np.float32)
-
Vt = csc_matrix(Vt, dtype=np.float32)
-
return U, S, Vt
-
-
def compute_estimated_matrix(self, urm, U, S, Vt, K, test):
-
rightTerm = S * Vt
# 对于每一个用户 U * S * Vt 得到的是他的坐标
-
max_recommendation =
250
-
estimatedRatings = np.zeros(shape=(self.MAX_UID, self.MAX_PID), dtype=np.float16)
-
recomendRatings = np.zeros(shape=(self.MAX_UID, max_recommendation), dtype=np.float16)
-
userTest = self.uTest
-
prod = U[userTest, :] * rightTerm
-
estimatedRatings[userTest, :] = prod.todense()
#得到用户U的预测评分列表
-
recomendRatings[userTest, :] = (-estimatedRatings[userTest, :]).argsort()[:max_recommendation]
#将评分表进行排序
-
#将计算结果按评分排序后返回
-
return recomendRatings
-
-
def get_ans(self):
-
self.process_data()
-
self.get_data_sparse()
-
U, S, Vt = self.compute_svd(self.urm, self.K)
-
uTest_recommended_items = self.compute_estimated_matrix(self.urm, U, S, Vt , self.K,
True)
-
user = self.uTest
-
ans_lis = []
-
for i
in uTest_recommended_items[user,
0:
10]:
#得到前十个评分最高的歌曲信息
-
song_details = self.small_set[self.small_set.so_index_value == i].drop_duplicates(
'so_index_value')[
-
[
'title',
'artist_name',
'year',
'release']]
-
ans_lis.append(list(song_details[
'title'])[
0])
#将歌曲名添加至答案列表中
-
return ans_lis
6.我学到了什么
1. 使用python的数据科学处理模块pandas进行数据预处理
当我们进行大数据项目的开发时,首要的第一步就是从网站上获取数据,这时候就有很多种方式了:使用网络爬虫,下载使用国内外的公开数据集等等。
拿到的数据集往往十分庞大,并且数据集中的数据往往存在数据冗余,数据缺失,数据错误等问题,我们想要进行模型的训练,当然要先对获取的海量数据进行数据预处理。
我们这次项目中拿到的数据文件类型是csv文件,对csv文件进行处理,可以使用python的强大数据科学分析库pandas来进行。首先我们import pandas as pd
-
import pandas
as pd
-
#(1)读取csv文件中的数据:(值得注意的是:encoding应该与使用记事本打开的编码保持一致)
-
data = pd.read_csv(文件名, encoding=
"utf-8")
#读取出来后是一个DataFrame类型
-
#(2)合并两个具有相同列的DataFrame
-
data_merge = pd.merge(data1, data2, on=共同的列名)
-
#(3)取出DataFrame中的某一列,并去除重复值(取出的是一个列表类型)
-
col = data_merge[列名].unique()
-
#(4)打印DataFrame的前几行
-
print(data.head(
10))
然后再配合excel中的“删除重复值”、“排序”等功能,就可以进行数据预处理,得到我们想要的数据啦
2. 使用python对文件夹中的海量图片进行批量命名
当文件夹中有成百上千张不规则命名的图片,我们如何将这些图片修改成统一的文件名格式呢?
贴一个大神博客链接:python批量修改文件夹中的图片名称
十分好用!相当牛逼!
3. django框架知识
Django是一个开放源代码的Web应用框架,由Python写成。采用了MTV的框架模式,即模型M,视图V和模版T。它最初是被开发来用于管理劳伦斯出版集团旗下的一些以新闻内容为主的网站的,即是CMS(内容管理系统)软件。并于2005年7月在BSD许可证下发布。这套框架是以比利时的吉普赛爵士吉他手Django Reinhardt来命名的。2019年12月2日,Django 3. 0发布
常用的Django命令:
django-admin startproject 程序名 #建立一个新的Django项目
django-admin startapp app名 #建立一个新的Django app
python manage.py runserver #运行建立的Django项目
python manage.py makemigrations #进行模型数据库文件的迁移, 记录我们对models.py的所有改动,并且将这个改动迁移到migrations这个文件下生成一个文件
python manage.py migrate #这条命令的主要作用就是把这些改动作用到数据库也就是执行migrations里面新改动的迁移文件更新数据库,比如创建数据表,或者增加字段属性
4. 推荐算法知识
(1)基于用户的协同过滤:UserCF 伪代码:
-
itemCF:要给user i推荐歌曲
-
1. 找出i听的所有歌曲的集合song_set_i[song1,song2,song3,...]
-
2. 找出歌曲集中的所有歌曲song_set_all[song1,........................]
-
3.
for song_i
in song_set_all :
-
找出听过歌曲song_i的用户的集合user_i[user1,user2,.....]
-
for song_j
in song_set_i :
-
找出听过歌曲song_j的用户的集合user_j[user1,user2,.....]
-
求出user_i 和 user_j 的交集 / 并集 的值,作为song_i和song_j的相似分,填在相似度矩阵coo中
-
song_i的得分:song_i 这一行的 所有 song_j 的相似分 的总和/song_set_i的大小
-
4. 将所有song的得分进行排序,返回前十名
-
(根据歌曲的受众,进行相似歌曲的推荐。其主要通过分析用户的行为记录计算物品之间的相似度。物品A和物品B具有很大的相似度是因为喜欢歌曲A的用户大都也喜欢歌曲B。)
(2)基于物品的协同过滤:ItemCF 伪代码:
-
userCF:要给user i 推荐歌曲
-
1. 找出user_i听的所有歌曲的集合song_set_i
-
2. 找出用户集中的所有用户user_set_all
-
3.
for user
in user_set_all :
-
找出用户user听过的歌曲的集合song_set_user
-
求出song_set_i和song_set_user的交集 / 并集 的值,作为user_i 和 user 的相似分
-
4. 将user_set_all中所有user的相似分进行排序,选出前十名user
-
5. 将前十名user听的歌曲返回,作为推荐的结果
-
(根据用户的相似度,进行推荐。与待推荐用户听歌记录相似的用户喜欢听的歌,很大可能上也会是待推荐用户喜欢听的歌)
(3)SVD分解
SVD分解就是用来将一个大的矩阵以降低维数的方式进行有损地压缩。应用在推荐系统中,我们的思路是利用新用户的评分向量找出该用户的相似用户。SVD分解就是将矩阵分解为U,S,V三个矩阵。k就是在进行矩阵降维时,选择的S矩阵中的特征值个数。对于新用户的评分,乘以U矩阵和S矩阵,得到他的降维结果。再与其余用户计算相似度,将与他最相似的用户评过分他却没评过分的歌曲排序,返回前几首作为推荐结果。
(4)冷启动(解决新用户的推荐策略)
将曲库中听的最多的歌曲推荐给新用户。
7. 心得和感悟
本次数据分析与挖掘课程设计让我亲身体验了一次开发完整的实际项目的经历。在本次课程设计中我负责推荐算法模块的开发设计。刚开始的几天,我一度觉得基于物品的推荐算法十分晦涩难懂。但我很快找到了解决的方案:用编译器自带的调试功能一步一步进行调试,观察每一个变量的变化情况。经过成百次的调试,我终于懂了算法的底层实现原理,然后又根据自己的理解手动实现了基于用户的推荐算法。虽然实现了算法,我们却又遇到了新的问题:算法的计算效率过慢,万级别的数据往往需要一分钟甚至是好几分钟才能够计算出答案。经过不断的打断点测试,计算运行时间,我们终于找到了时间复杂度过高的根源:建立倒排表这一步花费了大量的时间。在经过思考后,我决定将数据库中的数据按物品编号进行排序,并预处理每个物品的倒排表,将程序的运行时间缩短在十秒以内。经过学习和改进,又实现了更快的推荐算法:SVD(奇异值分解),经过一起努力,最终将推荐算法联通到了数据库与前端页面。每种算法背后都有着简洁的思想,正确的原理,将它们理解并运用到代码中,是我这次课程设计最大的收获。在这次课程设计中我其实获得了很多,也明白了很多我自己的缺陷。作为计算机系的学生,动手能力一定要强,所以有很多东西不一定到了用的时候再去学,而是要提前了解,提前去学,这样才能够真正地提高自己。
转载:https://blog.csdn.net/weixin_43727229/article/details/112528722