时光的褶皱距离上次更新Cuteen主题已有三百多个日夜。研究生生活如同被按下快进键的胶片，从去年九月开学起，论文开题、实习求职便像潮水般接踵而至。十一月完成开题答辩的那个深夜，我站在实验室窗前，突然意识到自己已在学术与现实的夹缝中跋涉了太久。于是开始自己找实习，很顺利的去了一家研究院。最近三个月在研究院的实习生活，让我对 "时光飞逝" 有了切肤之痛。从入职后没多久，大厂暑期实习投递就开始了。整个三月都在投简历，测评，笔试，面试。几乎全挂，只有美团给了2次面试机会，很可惜，都没通过，学历始终是一个过不去的坎，就像案底一样……截至目前0Offer。Cuteen主题也快有将近一年没更新了，实际上，从去年8月份开始，着手重构Cuteen，目前也快完成了，为了避免未来某一天服务器真的买不起，这次全面采用了静态博客的方案。由于变动很大，给主题起了一个新的名字：Soak。地址我放在下面，当全部功能完善后，就会发布：（自部署）http://soak.zwying.com（腾讯云EdgeOne平台）https://next.zwying.com我的未来下半年马上就要开始秋招，回想起两年前考上研究生的那一刻，说不尽的开心，甚至为了读研放弃了一次国考面试的机会，我记得当时想着：今天我能国考进面，三年后我一定也可以。殊不知这个岗位可能都不招人了……身边大部分师兄师姐都去当了信息技术老师，可是我站在人生的分岔路口，突然发现自己既没有他们那种对教育事业的热忱，也缺乏在企业厮杀的底气。当看到简历上的第一学历时，我想那些大厂HR婉拒理由是最清晰的。到底未来该选择怎样的一条路呢？我好像想起了疫情的时候，那时候给自己的一句话：代码比人心好懂。
在线手写数据集 CASIA-OLHWDB：联机手写识别的宝藏资源
CASIA简介CASIA是一个非常有价值的手写单字数据集。它涵盖了众多的汉字以及字母、数字和符号。由中科院自动化研究所在 2007-2010 年间收集；包含1020人书写的脱机（联机）手写中文单字样本和手写文本；联机数据是采用 Anoto 笔在点阵纸上书写后扫描、分割得到。主要由离线（HWDB）和在线（OLHWDB）两部分。具体可以前往官网了解：https://nlpr.ia.ac.cn/databases/handwriting/Home.html这里只介绍在线手写数据集部分，离线的网上介绍比较多。在线手写数据集单字主要包括OLHWDB1.0 与 OLHWDB1.2。一共收录汉字 7185 个，涵盖了 GB2312 中全部 6763 个汉字。这个数据集以.pot文件存储，其数据组织形式独特。这些版本的数据集可以用于训练和测试在线手写汉字识别模型。CASIA - OLHWDB 为手写汉字识别、字形分析等相关任务提供了丰富的数据资源。pot文件的解析pot文件的解析相关教程并不多，官网也只给出了文件头信息，和一个预览的软件。使用预览软件打开某个pot文件的结果：我这里按照文件头规则直接写了一版python的，会将在线的点集转换为json文件存储，方便后期使用，文件夹的命名使用的是这个汉字字符对应的md5编码，当然你也可以修改为其他的，解析时间很长，因为一个字符大概有100多个样本。import hashlib import json import os import struct def parse_pot_file(filename): with open(filename, 'rb') as f: while True: # 读取样本大小 sample_size_data = f.read(2) if not sample_size_data: break # 文件读取结束 sample_size = struct.unpack('<H', sample_size_data)[0] # 读取字符的 GBK 编码 tag_code_data = f.read(4) tag_code = struct.unpack('<I', tag_code_data)[0] gbk_code = tag_code & 0xFFFF # 提取低16位 character = gbk_code.to_bytes(2, 'big').decode('gbk', errors='ignore').strip('\x00') # print(f"Character: {character}") # c.append(character) # 读取笔画数量 stroke_count_data = f.read(2) stroke_count = struct.unpack('<H', stroke_count_data)[0] # print(f"Number of strokes: {stroke_count}") # 读取每个笔画 strokes = [] for _ in range(stroke_count): stroke = [] while True: # 读取一个坐标对 x, y = struct.unpack('<hh', f.read(4)) if (x, y) == (-1, 0): # 笔画结束标志 break stroke.append((x, y)) strokes.append(stroke) # 将字符编码为md5 md5_hash = hashlib.md5(character.encode('utf-8')).hexdigest() # 创建以MD5为名的文件夹 folder_path = os.path.join(output_dir, md5_hash) os.makedirs(folder_path, exist_ok=True) # 计算已有文件数量，确定当前文件名 existing_files = os.listdir(folder_path) json_filename = f"{len(existing_files) + 1}.json" # 将strokes保存为JSON文件 json_file_path = os.path.join(folder_path, json_filename) with open(json_file_path, 'w', encoding='utf-8') as json_file: json.dump(strokes, json_file, ensure_ascii=False, separators=(',', ':')) # print(f"Strokes: {strokes}") # 检查字符结束标志 (-1, -1) end_marker = struct.unpack('<hh', f.read(4)) if end_marker != (-1, -1): raise ValueError("Invalid character end marker.") output_dir = 'D:\\VeenCode\\MyCode\\DataSet\\OLHWDB\\PotTrainJson' input_dir = 'D:\\VeenCode\\MyCode\\DataSet\\OLHWDB\\Pot1.0Train' # 遍历文件夹中的所有 .pot 文件 for root, _, files in os.walk(input_dir): for file in files: if file.endswith('.pot'): file_path = os.path.join(root, file) parse_pot_file(file_path) print(os.path.join(root, file))文件夹Pot1.0Train用来存放全部pot文件，会将结果输出到PotTrainJson文件夹。结果预览将pot转换成json后，处理起来就方便太多了，随机来一首诗预览一下字符：下面是预览的代码，大家也可以拿去玩玩，使用了Tk库，在canvas上绘制的：import hashlib import json import tkinter as tk import os import random # 读取JSON文件并解析数据 def load_json(file_path): with open(file_path, 'r') as file: data = json.load(file) return data # 归一化点的坐标，放缩到90x90区域并居中 def normalize_data(strokes, offset_x, offset_y, word_size=90, box_size=100): # 获取所有点的最大值和最小值，以便归一化 all_x = [point[0] for stroke in strokes for point in stroke] all_y = [point[1] for stroke in strokes for point in stroke] min_x, max_x = min(all_x), max(all_x) min_y, max_y = min(all_y), max(all_y) # 归一化到90x90的绘制区域 scale_x = word_size / (max_x - min_x) if max_x - min_x != 0 else 1 scale_y = word_size / (max_y - min_y) if max_y - min_y != 0 else 1 # 计算居中的偏移量 center_x_offset = (box_size - word_size) / 2 center_y_offset = (box_size - word_size) / 2 normalized_strokes = [] for stroke in strokes: normalized_stroke = [] for x, y in stroke: norm_x = (x - min_x) * scale_x + offset_x + center_x_offset norm_y = (y - min_y) * scale_y + offset_y + center_y_offset normalized_stroke.append((norm_x, norm_y)) normalized_strokes.append(normalized_stroke) return normalized_strokes # 在canvas上绘制笔画 def draw_strokes(canvas, strokes): for i, stroke in enumerate(strokes): color = "red" if i == 0 else "black" for j in range(len(stroke) - 1): x1, y1 = stroke[j] x2, y2 = stroke[j + 1] canvas.create_line(x1, y1, x2, y2, fill=color, width=2) # 主函数：随机挑选10个JSON文件并渲染到画布上 def main(selected_files): # Canvas的大小设置为500x500 canvas_width = 500 canvas_height = 500 box_size = 100 # 每个字的框大小为100x100 word_size = 50 # 字的大小为90x90 # 创建Tkinter窗口和Canvas root = tk.Tk() root.title("Drawing Random Strokes") canvas = tk.Canvas(root, width=canvas_width, height=canvas_height) canvas.pack() # 按照5列2行的顺序排列和绘制笔画 num_cols = 5 for index, file_name in enumerate(selected_files): # 计算子区域的左上角的坐标 col = index % num_cols row = index // num_cols offset_x = col * box_size offset_y = row * box_size # 加载和归一化数据 strokes = load_json(os.path.join(folder_path, file_name)) normalized_strokes = normalize_data(strokes, offset_x, offset_y, word_size, box_size) # 在子区域内绘制笔画 draw_strokes(canvas, normalized_strokes) # 运行Tkinter主循环 root.mainloop() # 示例调用 if __name__ == "__main__": # 给定字符 chars = "白日依山尽黄河入海流欲穷千里目更上一层楼" json_path = [] # 遍历字符并生成对应的JSON文件路径 for char in chars: # 将字符md5加密 char_md5 = hashlib.md5(char.encode()).hexdigest() folder_path = os.path.join(r"D:\VeenCode\MyCode\DataSet\OLHWDB\PotTrainJson", char_md5) # 随机从folder_path文件夹中挑选一个json文件 json_files = [f for f in os.listdir(folder_path) if f.endswith('.json')] json_file = random.choice(json_files) json_path.append(os.path.join(folder_path, json_file)) main(json_path)
前言 距离开发第一款主题已经过去了4年之久，不得不感叹，时间过的真快啊！这4年里有3年是在疫情当中度过的，这4年也从一个本科生也混到了研究生。Cuteen主题用户也突破了1000+，这是远远超出我的预期的。 今天很高兴能够发布自己的新作品Furry！走了完全不同的风格路线，也不知道是自己年龄逐渐上升的原因还是怎么回事，越来越喜欢简洁的东西，少了一份年少时的繁杂与浮夸，多了一份成熟后的内敛与纯粹。 对于这款新的Furry主题，我注入了更多对生活与美的感悟。它不仅仅是一个主题，更像是我这一路成长的见证。在设计它的时候，我仿佛能看到自己这四年间的每一次转变与突破，每一次在迷茫中寻找方向，每一次在困境中坚守初心。如今，看着它以全新的面貌呈现在大家面前，心中既有欣慰，也有期待。 希望它能像Cuteen主题一样，受到大家的喜爱与认可，能在用户的心中留下属于它的独特印记。而我，也将带着这份对创作的热爱，继续在未来的岁月里探索前行，去追寻更多的可能，去创造更多能触动人心的作品。主题概述 当您目光所及，这款素雅而又别致的主题正静静地展现在您的眼前。它宛如一位身着灰色长裙的古典女子，用细腻的灰度色调为平淡的页面增添了几分内敛与沉静。精心设计的三栏布局，犹如一场文字与视觉的舞蹈，让每一个字句都在页面上轻盈地跃动，展现着它们独特的韵律。 全站PJAX与平滑滚动的完美结合，仿佛将您带入了一个丝滑如融化巧克力的梦幻世界。每一个滚动，每一个点击，都仿佛与主题产生了奇妙的共鸣，让您感受到前所未有的流畅与顺滑。右侧的音乐播放器，就像是一位贴心的朋友，在您需要的时候为您奏响轻松的旋律，让您的站点也充满了生活的气息。重新独立设计的设置页面，独立存储的自定义字段，保证了后台设置数据不再丢失，简洁规整的标签、精心设计的友情链接页、细致入微的时间线归档文章以及精准的Mathjax数学公式渲染，都在默默地诉说着这款主题的精致与匠心。它们犹如一颗颗璀璨的明珠，点缀在主题的每一个角落。这一切都在等待着您的到来……预览图 - 首页、归档页、黑暗模式[photos][/photos]后台设置预览[photos][/photos]功能统计功能效果自适应任何设备几乎都能完美适配字体优化享受文字的美代码高亮Mac样式代码高亮，多语言支持，一键复制数学公式Mathjax强力加持，全面支持渲染LaTeX公式友情链接页面独立的友链页面，通过短代码加持，更快添加友情链接文章归档独立合并折叠面板式文章归档页面，统计博客的内容平滑滚动平滑滚动功能让您的鼠标滚轮像上了油一般丝滑Pjax几乎感受不到页面切换之间的卡顿Ajax评论采用Ajax评论技术，带给您更好的体验打赏功能后台可配置打赏功能，网络恰饭乞讨必备点赞功能文章点赞功能，访问者可以给喜欢的文章点赞自定义静态CDN可以自定义CDN链接，加快您网站的访问速度预览地址https://furry.zwying.com主题购买项目价格Furry主题68元安装指导（可选）+12元[RainBowText]Cuteen老用户终身享受折扣价购买！！！[/RainBowText] 收费是为了后续能够更好的维护，也是对我极大的鼓励，感谢您的支持👏。如您考虑好，请仔细阅读下面的购买须知。 [RainBowText]曾经有一份诱人的价格摆在我面前，但我没有珍惜，等到失去了我才后悔莫及，尘世间最痛苦的事莫过于此。[/RainBowText][card title="购买须知" color="primary"]0.您需满足以下环境条件：Typecho1.2.1、MySQL5+、PHP>=8.0；为了保证使用体验，请使用服务器部署。1.Furry主题不适用于 WordPress、Hexo 等其他非 Typecho 博客程序平台。2.谢绝议价，加好友请注明「购买 Furry 主题」，百元以内只想和您交个朋友。3.购买主题后，您将获得主题的最新版本，后续所有的升级版本均为免费。本主题部分核心代码进行了加密处理，需绑定域名授权，购买授权后可随意使用，但不得有任何泄露源代码行为，包括不限于分发、转售、上传至开源平台、代替他人购买。如有以上行为，不再提供主题更新等售后服务。因主题是可复制的商品，在代码发出后不予退款，敬请谅解。4.主题自用随意修改，但请勿二次修改后转售！模板没有许可移植任何平台，请勿购买主题后擅自移植其他平台。如有以上行为，不再提供主题更新等售后服务。5.售后服务包括您在使用本主题时遇到的问题，但不包含主机及 Typecho 的主题安装、使用教程，也不提供主题的个性化定制服务，如有需求，单独联系，世界加钱可及。6.域名授权主域名一个（满足条件的情况下可更换），授权主域名下的二级域名不限，不管是一级域名还是二级域名均需要联系我添加授权，请仔细考虑后购买。7.由于远程授权，本主题不可在本地搭建测试环境，请仔细考虑后购买。8.严禁将本主题使用在涉及政治、宗教、黄赌毒等站点，如有以上行为，立即查封，并向相关部门报案。9.购买人即为第一责任人，购买后即默认同意此《购买须知》，出现违反《购买须知》的行为，追究相关责任。[/card][card title="购买方式" color="success"]联系QQ：**2013143650**咨询主题相关问题请发送邮件到：zwying0814#qq.com (#号换成@)1.购买前请各位客户通过任意一种方式先联系到我，土豪切勿先付款后联系。2.购买后会被邀请至Furry主题交流群，享受主题售后服务，永久包更新。[/card]
近日，在刷蓝桥杯中的模拟实战赛时，有这么一道题：叫“汽车私人定制”，题目链接如下。https://www.lanqiao.cn/problems/18456/learning/?contest_id=187 打开css的文件，可以看到代码都是类似scss这种： 最开始以为是使用了什么预编译的技术方案，后来才知道，原来原生浏览器已经开始支持css嵌套语法了，关键词是CSS Nesting。 2024年，可以看到基本上市面上大部分主流浏览器都支持CSS Nesting，也就是说这确确实实成为了一种既定的规范。语法/* Without nesting selector */ parent { /* parent styles */ child { /* child of parent styles */ } } /* With nesting selector */ parent { /* parent styles */ & child { /* child of parent styles */ } } /* the browser will parse both of these as */ parent { /* parent styles */ } parent child { /* child of parent styles */ } 可以看到语法规则和less、scss非常类似。了解更多可以参考MDN：https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting/Using_CSS_nesting注意事项虽然语法很类似，但需要注意：Concatenation is not possible（无法像 less 或 sass 利用父选择器和其他字符串组合）比如下面这一段： tag p 预期结果：.css-nesting-concatenation .css-nesting-concatenation-p {color: blue;} 实际结果：找不到元素，未生效。 根据 MDN 解释，& 选择只会被当做 选择器 来使用Invalid nested style rules - （嵌套样式内 遇到无效样式的处理） 如何处理在嵌套模式内无效样式？看一下MDN 中，可以看出示例，在遇到无效样式时，会忽略无效样式，继续解析下一个有效样式。.parent { /* .parent styles these work fine */ & %invalid { /* %invalid styles all of which are ignored */ } & .valid { /* .parent .valid styles these work fine */ } } 但这里要注意，不同浏览器处理的结果可能会有不一致的情况，比如下面这个例子： tag i tag p tag a 在我本机的Chrome和Firefox上表现不一致，Firefox和MDN描述的是一致的。最后想说的 在web发展的过程中，有很多API都是浏览器吸收了开源社区的热门库后出现的，比如 document.querySelector 吸收了 jQuery 的精华，或者Promise，JSON的相关API都是先出现在社区，然后被浏览器原生支持。今天的css原生嵌套也是借鉴了less，sass等社区方案而出现的。这才是共同进步。
蓝桥杯十五届省赛Web应用开发题目答案解析
去年报了个蓝桥杯Web，昨天刚打完省赛，10道题基本无压力 ::(呵呵) （[DarkBText]果然是圈钱杯[/DarkBText]），最后一道可能会扣点分，整理一下赛后直播的答案，也包括职业院校组的，给需要的小伙伴们：1. 智能停车系统 ⭐️考点： flex 布局解题答案：.cars { position: absolute; z-index: 2; display: flex; width: 600px; height: 600px; flex-direction: column; /* TODO: 请在下方编写题目代码，不要更改其他选择器里的代码 */ flex-wrap: wrap; align-content:space-between; justify-content:space-between; }2. 分享点滴 ⭐️考察：对象的遍历，字符串基本操作答案1：function appendParamsToURL(url, params) { // 创建一个空字符串来存储参数字符串 let str = ""; // 遍历参数对象中的每个键 for (const key in params) { // 将键值对追加到字符串中 str += `${key}=${params[key]}&`; } // 移除字符串末尾的 "&" 符号 str = str.slice(0, -1); // 判断是否原始 URL 已包含查询字符串（通过是否包含 "?" 符号来判断） // 如果已包含查询字符串，则设置 sym 变量为 "&"，用于以 "&" 作为分隔符追加参数 // 否则，设置 sym 变量为 "?"，作为查询字符串的起始符号 let sym = url.includes("?") ? "&" : "?"; // 连接原始 URL、sym 符号和参数字符串，并返回最终的 URL return `${url}${sym}${str}`; }答案2：function appendParamsToURL(url, params) { // 使用 Object.entries() 获取参数对象的所有键值对并按照规则拼接 const paramArray = Object.entries(params).map( ([key, value]) => `${key}=${value}` ); // 使用join方法拼接参数字符串 const paramString = paramArray.join("&"); // 判断URL中是否已包含参数 let sym = url.includes("?") ? "&" : "?"; // 返回拼接后的URL return `${url}${sym}${paramString}`; }3. 布局切换 ⭐️考察 DOM 操作、class 类名操作 option.addEventListener('click', function () { for (const otherOption of layoutOptions) { // 移除其他选项的 active 类 otherOption.classList.remove('active'); } // TODO：待补充代码 this.classList.add('active'); // 为当前选项添加active类 }4. 产品 360 度展示 ⭐️⭐️考察：Promise、循环答案1：const pipeline = async (initialValue, sequence) => { let res = initialValue; for (const fn of sequence) { res = await fn(res); } return res; };答案2：const pipeline = (initialValue, sequence) => { let res = Promise.resolve(initialValue); for (const fn of sequence) { res = res.then((result) => fn(result)); } return res; };5. 多表单验证 ⭐️考察element-plus组件的使用const validateName = (rule, value, callback) => { if (value === "") { callback(new Error("请输入姓名")); } else { const reg = /[^\u4e00-\u9fa5]/g; if (reg.test(value)) { callback(new Error("只能输入汉字")); } callback(); } }; // TODO-3：书写表单校验规则，并绑定到对应表单上 const rules = reactive({ name: [ { required: true, message: "请输入姓名", trigger: "blur" }, { validator: validateName, trigger: "blur" }, ], sex: [{ required: true, message: "请选择性别", trigger: "change" }], age: [{ required: true, message: "请输入年龄", trigger: "blur" }], isCompetition: [ { required: true, message: "请选择是否参加过编程比赛", trigger: "change" }, ], isEntrepreneurship: [ { required: true, message: "请选择是否有过创业经历", trigger: "change" }, ], });6. 找回连接的奇幻之旅 ⭐️⭐️⭐️考察：函数封装、闭包function resetableOnce(fn) { let done = false; // 标记函数是否已执行 let result; // 保存函数执行的结果 /** * 保证只执行一次的函数 * @returns {Any} 返回函数fn的执行结果 */ function runOnce() { if (!done) { // 如果之前没有执行过，则执行函数fn，并保存结果 result = fn.apply(this, arguments); done = true; // 标记为已完成，确保之后不再执行 } // 返回之前执行的结果 return result; } /** * 重置函数的执行状态 */ function reset() { done = false; // 重置done为false，允许函数再次执行 } // 返回控制对象 return { runOnce, reset }; }7. 悠然画境 ⭐️⭐️考察数组操作、字符串操作function generateAndDisplayImages(imageCount, selectedText) { // TODO：待补充代码 根据数量和用户输入在页面上生成图片并显示到 .right-panel 元素中 let count = 0; artDataArray.forEach((el) => { let tags = el.tags.split("、"); tags.forEach((tag) => { if (selectedText.includes(tag)) { count++; } }); el["count"] = count; count = 0; }); artDataArray.sort((a, b) => { return b.count - a.count; // 从小到大 -> a.age - b.age }); let b = artDataArray.slice(0, imageCount); return b; }8. 简易 JSX 解析器 ⭐️⭐️考察 dom 操作、字符串方法/** * 将驼峰式命名转换为短横线命名 * @param {string} str - 驼峰式命名的字符串 * @returns {string} - 转换后的短横线命名字符串 */ function camelToDash(str) { let result = ""; let hasUppercase = false; // 标记字符串中是否包含大写字母 for (let i = 0; i < str.length; i++) { let char = str.charAt(i); // 检查当前字符是否为大写字母 if (char === char.toUpperCase()) { hasUppercase = true; if (i > 0) { result += "-"; // 在大写字母前添加短横线 } result += char.toLowerCase(); // 将大写字母转换为小写字母并添加到结果中 } else { result += char; // 如果不是大写字母，直接添加到结果中 } } // 如果字符串中没有大写字母，则直接返回原字符串 if (!hasUppercase) { return str; } return result; } const jsx = (type, config) => { /* TODO: 请完善 jsx 函数的代码实现 */ const dom = document.createElement(type); for (let key in config) { const prop = config[key]; if (key === "style") { // 处理样式 let styleStr = Object.entries(prop) .map((item) => { return `${camelToDash(item[0])}:${item[1]}`; }) .join(";"); dom.setAttribute("style", styleStr); } else if (typeof prop === "function") { dom[key] = prop; } else if (key === "children") { // 处理 children if (typeof prop === "string") { dom.appendChild(document.createTextNode(prop)); } else { prop.forEach((child) => typeof child === "string" ? dom.appendChild(document.createTextNode(child)) : dom.appendChild(child) ); } } else { // 处理其他属性 dom.setAttribute(key, prop); } } return dom; };9. tree 命令助手 ⭐️⭐️⭐️考察 fs、递归函数function generateTree(dirPath) { // 读取目录下的所有文件和文件夹 const files = fs.readdirSync(dirPath); const tree = []; files.forEach(file => { const filePath = path.join(dirPath, file); const isDirectory = fs.statSync(filePath).isDirectory(); if (isDirectory) { // 如果是目录，则递归生成子目录的文件树 const subtree = generateTree(filePath); tree.push({ name: file, children: subtree }); } else { // 如果是文件，则直接添加到文件树 tree.push({ name: file }); } }); return tree; }10. Github 明星项目统计 ⭐️⭐️⭐️考察 vue3 v-for、Echarts 数据处理 {{language}} 11. 小蓝驿站 ⭐️⭐️⭐️考察 vue3 v-for、ref、数据处理// 目标 1 0" class="contacts-group-title"> {{ group.letter }} {{ contact.name }} 删除 12. 商品浏览足迹 ⭐️⭐️⭐️考察 axios 请求、数据处理window.onload = async ()=> { const res = await axios("./js/data.json"); const newData = getData(res.data); showData(newData); }; /** * 将同一天浏览的相同商品去重并作为数组返回 * @param {Array} defaultData json 文件中读取到的原始数据 * @returns 去重后的数据，数据结构与 defaultData 相同 */ const removeDuplicates = defaultData => { let newData = []; // TODO：在下面补充代码，最终完成该函数封装 defaultData.forEach((defaultGoods) => { const defaultDate = new Date(defaultGoods.viewed_on); const dD = `${defaultDate.getFullYear()}-${ defaultDate.getMonth() + 1 }-${defaultDate.getDate()}`; defaultGoods.date = dD; let hadThisGoods = false; newData.forEach((tmpGoods) => { const tmpDate = new Date(tmpGoods.viewed_on); const tD = `${tmpDate.getFullYear()}-${ tmpDate.getMonth() + 1 }-${tmpDate.getDate()}`; if (tmpGoods.id == defaultGoods.id && tD == dD) { hadThisGoods = true; } }); if (!hadThisGoods) { newData.push(defaultGoods); } }); return newData; } /** * 将去重后的数据根据字段 viewed_on（格式化为 YYYY-MM-DD） 降序排序 * @param {*} defaultData 去重后的数据 * @returns 根据字段 viewed_on（格式化为 YYYY-MM-DD） 降序排序 */ const sortByDate = defaultData => { let newData = []; // TODO：在下面补充代码，最终完成该函数封装 function sortByProperty(property, asc) { return function (value1, value2) { let a = new Date(value1[property]); let b = new Date(value2[property]); // 默认升序 if (asc == undefined) { return a - b; } else { return asc ? a - b : b - a; } }; } newData = defaultData.sort(sortByProperty("date", false)); return newData; } /** * 将去重排序后的所有商品数据，作为一个对象存储并返回， * 该对象的所有 `key` 为浏览商品的当天日期（即，字段 viewed_on 格式化为 YYYY-MM-DD）， * `value` 为当天浏览的所有商品（以数组形式表现）。 * @param {Array} defaultData 重后的所有商品数据 * @returns */ const transformStructure = defaultData => { let newData = {}; // TODO：在下面补充代码，最终完成该函数封装 // 月份和日期 不足10的前面补0 defaultData.forEach(item=>{ let date=new Date(item.date) ; let y = date.getFullYear() let m = date.getMonth()+1 m=m<10?'0'+m:m let d = date.getDate(); d=d<10?'0'+d:d item.date=`${y}-${m}-${d}`; }) newData = defaultData.reduce((aD, item) => { console.log(item); if (!aD[item.date]) { aD[item.date] = []; aD[item.date].push(item); } else { if (!aD[item.date].includes(item.date)) { aD[item.date].push(item); } } return aD; }, {}); return newData; }13. 扫雷游戏 ⭐️⭐️⭐️⭐️考察二维数组操作、递归function mineSweeperAlgorithms(arr, { row, col }) { if (flagData[row][col]) return; flagData[row][col] = true; const { positionWithoutMineArr, count } = getAroundAndCount(arr, { row, col, }); if (count == 0) { // 如果当前格子周围没有地雷，递归执行扫雷算法 positionWithoutMineArr.forEach((around) => { mineSweeperAlgorithms(arr, around); }); } }14.NPM Download Simulator ⭐️⭐️⭐️⭐️考察：获取数据类型、 promise.racefunction myRace(iterable) { return new Promise((resolve, reject) => { if (!(Symbol.iterator in Object(iterable))) { reject( new TypeError( `${Object.prototype.toString .call({ iterable }) .slice(-8, -1) .toLocaleLowerCase()} is not iterable` ) ); return; } const promises = Array.from(iterable); let resolvedOrRejected = false; for (const promise of promises) { Promise.resolve(promise).then( (value) => { if (!resolvedOrRejected) { resolvedOrRejected = true; resolve(value); } }, (reason) => { if (!resolvedOrRejected) { resolvedOrRejected = true; reject(reason); } } ); } }); }当然，web只要最终实现的效果和题目中描述的一致，那就能得分，上面答案只是参考。总体感觉难度不大，希望能进一波国赛吧
手撕BP神经网络之参数更新数学推导
[RainBowText]揭开这个神秘的面纱，探索其中的数学奥秘[/RainBowText]BP神经网络概念 首先从名称中可以看出，BP神经网络可以分为两个部分，BP和神经网络。BP是Back Propagation的简写 ，意思是反向传播。BP网络能学习和存贮大量的输入-输出模式映射关系，而无需事前揭示描述这种映射关系的数学方程。它的学习规则是使用最速下降法也称为梯度下降法，通过反向传播来不断调整网络的权值和阈值，使网络的误差平方和最小。 其主要的特点是：信号是正向传播的，而误差是反向传播的。 举一个例子，某厂商生产一种产品，投放到市场之后得到了消费者的反馈，根据消费者的反馈，厂商对产品进一步升级，优化，一直循环往复，直到实现最终目的——生产出让消费者更满意的产品。产品投放就是“信号前向传播”，消费者的反馈就是“误差反向传播”。这就是BP神经网络的核心。[card title="🚨有几点需要注意的" color="warning"]一般在提到有多少层时，不包括输出层，只计算隐藏层和输出层在一起的数量一般来说，越深的网络越能学到更深的特征逻辑回归（Logistic）其实是神经网络的一个特例，即不包含隐藏层的神经网络[/card]训练一个BP神经网络的核心步骤 这张图基本反应了训练一个BP神经网络的核心步骤，很容易懂，不过多解释了。下面，我们以一个最简单的BP神经网络为例，来推导其计算过程。见下图：前向传播隐含层输入节点：$$ \begin{aligned} h_{1}=W_{11}\cdot x_{1}+W_{21}\cdot x_{2}+b_{h1}\\ h_{2}=W_{12}\cdot x_{2}+W_{22}\cdot x_{2}+b_{h2}\\ h_{3}=W_{13}\cdot x_{3}+W_{23}\cdot x_{2}+b_{h3} \end{aligned} $$隐含层输出节点：经过激活函数$f\left( x\right) =\dfrac{1}{1+e^{-x}}$运算得到：$$ \begin{aligned} h_{o1}=f\left( h_{1}\right) \\ h_{o2}=f\left( h_{2}\right) \\ h_{o3}=f\left( h_{3}\right) \\ \end{aligned} $$这里提一点激活函数的导数，后面会用到：$$ f^{'}(x)=\frac{0-(-e^{-x})}{(1+e^{-x})^{2}}=\frac{1}{1+e^{-x}}\times (1-\frac{1}{1+e^{-x}})=f(x)(1-f(x)) $$输出层输入节点：$$ \begin{aligned} y_{1}=W_{ho1}\cdot h_{o1}+W_{ho2}\cdot h_{o2}+W_{ho3}\cdot h_{o3}+b_{o} \end{aligned} $$输出层输出节点：$$ y_{o}=f\left( y_{1}\right) $$反向传播需要明白微积分中的链式法则，设$x$是实数，$f$和$g$是从实数映射到实数的函数。假设$y=g(x)$并且$z=f(g(x))=f(y)$。那么链式法则就是：$$ \frac{dz}{dx}=\frac{dz}{dy}\frac{dy}{dx} $$在算法中，会计算实际输出$y_{o}$和期望输出$d_{o}$的误差，如果这个误差大于设定的阈值，那么就会进行误差的反向传播。定义损失函数：$$ f_{error}=\dfrac{1}{2}\sum \left( d_{o}-y_{o}\right) ^{2} $$损失函数中的$1/2$是为了后面求导计算方便而添加的。🎯目标：更新$W$、$b$使$f_{error}$更小输出层→隐藏层的更新先看输出层，假设更新后的$b_{o}$为$b_{o}^{'}$，即$b_{o}^{'} = b_{o}+\Delta b_{o}$，只要求出$\Delta b_{o}$，就能得到$b_{o}^{'}$。根据梯度下降的原则，可以知道：$$ \Delta b_{o}=\eta \cdot \dfrac{\partial f_{error}}{\partial b_{o}} $$根据上面的推导，可以知道：$$ f_{error}=\dfrac{1}{2} \left( d_{o}-y_{o}\right) ^{2}=\dfrac{1}{2} (d_{o}-f(W_{ho1}\cdot h_{o1}+W_{ho2}\cdot h_{o2}+W_{ho3}\cdot h_{o3}+b_{o}))^2 $$根据链式法则：$$ \dfrac{\partial f_{error}}{\partial b_{o}}=\dfrac{\partial f_{error}}{\partial y_{o}}\cdot \dfrac{\partial y_{o}}{\partial b_{o}}=\dfrac{\partial f_{error}}{\partial y_{o}}\cdot \dfrac{\partial y_{o}}{\partial y_{1}}\cdot \dfrac{\partial y_{1}}{\partial b_{o}} $$下面单独求解各个偏导：$$ \dfrac{\partial f_{error}}{\partial y_{o}}=\dfrac{\partial (\dfrac{1}{2} \left( d_{o}-y_{o}\right) ^{2})}{\partial y_{o}}=-(d_{o}-y_{o}) $$$$ \dfrac{\partial y_{o}}{\partial y_{1}}=\dfrac{\partial (\dfrac{1}{1+e^{-y_{1}}})}{\partial y_{1}}=y_{o}(1-y_{o}) $$$$ \dfrac{\partial y_{1}}{\partial b_{o}}=\dfrac{\partial (W_{ho1}\cdot h_{o1}+W_{ho2}\cdot h_{o2}+W_{ho3}\cdot h_{o3}+b_{o})}{\partial b_{o}}=1 $$所以：$$ \dfrac{\partial f_{error}}{\partial b_{o}}=-(d_{o}-y_{o})\cdot y_{o}\cdot (1-y_{o}) $$所以：$$ \Delta b_{o}=\eta \cdot \dfrac{\partial f_{error}}{\partial b_{o}}=-\eta(d_{o}-y_{o})\cdot y_{o}\cdot (1-y_{o}) $$对于$W$，这里以$W_{ho1}$为例子，其他同理可得。由链式法则：$$ \dfrac{\partial f_{error}}{\partial W_{ho1}}=\dfrac{\partial f_{error}}{\partial y_{o}}\cdot \dfrac{\partial y_{o}}{\partial W_{ho1}}=\dfrac{\partial f_{error}}{\partial y_{o}}\cdot \dfrac{\partial y_{o}}{\partial y_{1}}\cdot \dfrac{\partial y_{1}}{\partial W_{ho1}} $$式中的$\dfrac{\partial f_{error}}{\partial y_{o}}$和$\dfrac{\partial y_{o}}{\partial y_{1}}$已经计算过，无需再次计算。$$ \dfrac{\partial y_{1}}{\partial W_{ho1}} = \dfrac{\partial (W_{ho1}\cdot h_{o1}+W_{ho2}\cdot h_{o2}+W_{ho3}\cdot h_{o3}+b_{o})}{\partial W_{ho1}}=h_{o1} $$所以：$$ \Delta W_{ho1}=\eta \cdot \dfrac{\partial f_{error}}{\partial W_{ho1}}=-\eta(d_{o}-y_{o})\cdot y_{o}\cdot (1-y_{o})\cdot h_{o1} $$这样就把输出层的$W$、$b$更新完成了，更新后的结果：$$ b_{o}^{'} = b_{o}+\Delta b_{o}=b_{o}-\eta(d_{o}-y_{o})\cdot y_{o}\cdot (1-y_{o}) $$$$ \begin{aligned} W_{oh1}^{'} = W_{oh1}+\Delta W_{oh1}=W_{oh1}-\eta(d_{o}-y_{o})\cdot y_{o}\cdot (1-y_{o})\cdot h_{o1}\\ W_{oh2}^{'} = W_{oh1}+\Delta W_{oh2}=W_{oh2}-\eta(d_{o}-y_{o})\cdot y_{o}\cdot (1-y_{o})\cdot h_{o2}\\ W_{oh3}^{'} = W_{oh1}+\Delta W_{oh3}=W_{oh3}-\eta(d_{o}-y_{o})\cdot y_{o}\cdot (1-y_{o})\cdot h_{o3}\\ \end{aligned} $$隐藏层→输入层的更新这里以$b_{h1}$为例子。此时：$$ \begin{aligned} f_{\text{error}} &= \frac{1}{2} \left( d_{o} - y_{o} \right)^{2} \\ &= \frac{1}{2} \left( d_{o} - f \left( W_{ho1} \cdot f \left( x_{1} \cdot W_{11} + x_{2} \cdot W_{21} + b_{h1} \right) + W_{ho2} \cdot f \left( x_{1} \cdot W_{12} + x_{2} \cdot W_{22} + b_{h2} \right) \right. \right. \\ &\quad \left. \left. + W_{ho3} \cdot f \left( x_{1} \cdot W_{13} + x_{2} \cdot W_{23} + b_{h3} \right) + b_{o} \right) \right)^{2} \end{aligned} $$由链式法则：$$ \dfrac{\partial f_{error}}{\partial b_{h1}}=\dfrac{\partial f_{error}}{\partial y_{o}}\cdot \dfrac{\partial y_{o}}{\partial b_{h1}}=\dfrac{\partial f_{error}}{\partial y_{o}}\cdot \dfrac{\partial y_{o}}{\partial y_{1}}\cdot \dfrac{\partial y_{1}}{\partial h_{o1}}\cdot \dfrac{\partial h_{o1}}{\partial b_{h1}} $$式中的$\dfrac{\partial f_{error}}{\partial y_{o}}$和$\dfrac{\partial y_{o}}{\partial y_{1}}$已经计算过，无需再次计算。$$ \dfrac{\partial y_{1}}{\partial h_{o1}}=\dfrac{\partial (W_{ho1}\cdot h_{o1}+W_{ho2}\cdot h_{o2}+W_{ho3}\cdot h_{o3}+b_{o})}{\partial h_{o1}}=W_{ho1} $$$$ \dfrac{\partial h_{o1}}{\partial b_{h1}}=\dfrac{\partial(f(W_{11}\cdot x_{1}+W_{21}\cdot x_{2}+b_{h1}))}{\partial b_{h1}}=h_{o1}(1-h_{o1}) $$所以：$$ \Delta b_{h1}=\eta \cdot \dfrac{\partial f_{error}}{\partial b_{h1}}=-\eta(d_{o}-y_{o})\cdot y_{o}\cdot (1-y_{o})\cdot W_{ho1} \cdot h_{o1}(1-h_{o1}) $$同理，可以得到：$$ \Delta b_{h2}=\eta \cdot \dfrac{\partial f_{error}}{\partial b_{h2}}=-\eta(d_{o}-y_{o})\cdot y_{o}\cdot (1-y_{o})\cdot W_{ho2} \cdot h_{o2}(1-h_{o2}) $$$$ \Delta b_{h3}=\eta \cdot \dfrac{\partial f_{error}}{\partial b_{h3}}=-\eta(d_{o}-y_{o})\cdot y_{o}\cdot (1-y_{o})\cdot W_{ho3} \cdot h_{o2}(1-h_{o3}) $$对于$W$，这里以$W_{11}$为例子，其他同理可得。由链式法则：$$ \dfrac{\partial f_{error}}{\partial W_{11}}=\dfrac{\partial f_{error}}{\partial y_{o}}\cdot \dfrac{\partial y_{o}}{\partial W_{ho1}}=\dfrac{\partial f_{error}}{\partial y_{o}}\cdot \dfrac{\partial y_{o}}{\partial h_{o1}}\cdot \dfrac{\partial h_{o1}}{\partial W_{11}}=\dfrac{\partial f_{error}}{\partial y_{o}}\cdot \dfrac{\partial y_{o}}{\partial y_{1}}\cdot\dfrac{\partial y_{1}}{\partial h_{o1}}\cdot \dfrac{\partial h_{o1}}{\partial W_{11}} $$式中的$\dfrac{\partial f_{error}}{\partial y_{o}}$、$\dfrac{\partial y_{o}}{\partial y_{1}}$和$\dfrac{\partial y_{1}}{\partial h_{o1}}$已经计算过，无需再次计算。$$ \dfrac{\partial h_{o1}}{\partial W_{11}}=\dfrac{\partial h_{o1}}{\partial h_{1}}\cdot \dfrac{\partial h_{1}}{\partial W_{11}}=h_{1}(1-h_{1})\cdot x_{1} $$所以：$$ \Delta W_{11}=\eta \cdot \dfrac{\partial f_{error}}{\partial W_{11}}=-\eta(d_{o}-y_{o})\cdot y_{o}\cdot (1-y_{o})\cdot W_{ho1} \cdot h_{1}(1-h_{1})\cdot x_{1} $$同理，可以得到：$$ \Delta W_{12}=\eta \cdot \dfrac{\partial f_{error}}{\partial W_{12}}=-\eta(d_{o}-y_{o})\cdot y_{o}\cdot (1-y_{o})\cdot W_{ho2} \cdot h_{2}(1-h_{2})\cdot x_{1} $$$$ \Delta W_{13}=\eta \cdot \dfrac{\partial f_{error}}{\partial W_{13}}=-\eta(d_{o}-y_{o})\cdot y_{o}\cdot (1-y_{o})\cdot W_{ho3} \cdot h_{3}(1-h_{3})\cdot x_{1} $$$$ \Delta W_{21}=\eta \cdot \dfrac{\partial f_{error}}{\partial W_{21}}=-\eta(d_{o}-y_{o})\cdot y_{o}\cdot (1-y_{o})\cdot W_{ho1} \cdot h_{1}(1-h_{1})\cdot x_{2} $$$$ \Delta W_{22}=\eta \cdot \dfrac{\partial f_{error}}{\partial W_{22}}=-\eta(d_{o}-y_{o})\cdot y_{o}\cdot (1-y_{o})\cdot W_{ho2} \cdot h_{2}(1-h_{2})\cdot x_{2} $$$$ \Delta W_{23}=\eta \cdot \dfrac{\partial f_{error}}{\partial W_{23}}=-\eta(d_{o}-y_{o})\cdot y_{o}\cdot (1-y_{o})\cdot W_{ho3} \cdot h_{3}(1-h_{3})\cdot x_{2} $$这样就把输入层的$W$、$b$更新完成了。下一篇开始手撕代码！
南京师范大学2023教育信息工程考研复试经验分享
接上一篇初试经验分享文章： 南京师范大学2023教育信息工程考研初试经验分享 。 时间过得很快，转眼间研一上已经结束了，周一的时候（2月26号）是2024年考研出分的日子，因此，我觉得这一篇去年的复试经验分享现在很有必要写出来给大家。老样子，还是先说一下自己的复试结果：笔试130，面试93（第一），最终综合排名第三顺利录取。初试的时候我是排名第四，所以，复试还是有机会往上继续冲一冲的。 南师在分数出来一周后，会出来专业排名，这个排名是总分排名，意思是即使国家线有没过的，也会有这个排名。可能会出现自己的最终排名会比这个显示的排名高的情况，原因是前面可能有国家线单科没有通过的情况。 复试通知会在教科院的官网发布，请及时关注官网通知，没事刷一刷，看看通知出来没。这是去年的复试通知：2023年南京师范大学教育科学学院硕士研究生复试录取工作办法。复试内容 在23年之前，由于疫情的原因，复试全部在线上进行，没有笔试这一项。但从23年开始，又恢复了正常。教育信息工程专业复试分为两个部分，笔试+面试。最终录取结果五五开，也就是初试成绩×50%+复试成绩×50%。复试笔试难度没有初试专业课这么高，参考书目如下：F045 现代信息处理方法基础：《C语言程序设计》(第5版), 谭浩强, 清华大学出版社, 2017年;《数据结构与算法图解》，Jay Wengrow著, 袁志鹏译, 人民邮电出版社, 2019年；《人工智能导论》(前8章)，李德毅，中国科学技术出版社，2018年；《高等数学》(第7版，上册)，同济大学数学系编，高等教育出版社，2014年。 笔试满分150分，认真复习拿个130+还是没问题的。高等数学知识考察的比较浅，如果忘记，可以看一遍B站的高数上册期末速成课。重点放在程序设计，以及对程序、人工智能的理解上。去年最后一道笔试题是开放性的题目。 对于面试，主要由两个部分构成，英语口试和专业交流。 我记得当时我复试的时候很紧张，其实可以放松自己，太紧张说话就可能会抖（不知道你们有没有这种情况，我有），但是好在思路都是ok的。英语口试可以每天早上清醒的时候练，对着镜子，或者录视频，看回放，找出自己哪里不足。如果有小伙伴能陪你一起，那就太好了，可以相互练习。我当时没有这个条件，由于在上班，我就整理了一份常见的问题，每天上班的路上，都挑一个出来背，晚上下班的时候再复习一遍。 专业交流这个变数就很大了，什么问题都有可能会被问到，但老师们会从这些问题当中或深或浅的考察你的科研能力，和科研水平，以及对于基础知识的掌握。给的参考书看完的情况下，如果还有空余时间，建议把线性代数、操作系统、数据库这三门书找出来过一遍。教育信息工程比较偏向于算法。可能会问到计算机的一些基础知识，比如我去年就问到了操作系统中的死锁、管态和目态这种复试参考书根本没有的知识，都是以前学的基础知识。常见问题解答1.先口试还是先问专业问题？ 答：去年我是先问专业问题，最后口试。当时让我也挺意外的，感觉没有按套路来，说了一句：自我介绍一下吧。我犹豫了一下，脑海竟然还在想是用中文还是英文，可能当时太紧张了 ::(狂汗) 。其实到英语部分会有老师和你说的。再具体一点，听到中文用中文，听到英文用英文。2.复试要带简历吗？ 答：可有可无。如果有，一定要保证简历上的内容滚瓜烂熟，包括你写上去的以前学习过的课程。也就是说，写上的，一定都是你熟稔于心的。我当时是唯一一个带简历的，写上了以前修过的课程，有些老师就会问到这些课程的知识。3.要提前联系导师吗？ 答：作用不大，一切以分数为主，还是非常公平的。如果排名出来了，确定自己一定能进复试了，发一下也可以。去年我是确定自己能够进复试后，给导师发了封邮件。得到的回复是：期待复试能有良好的表现。4.遇到不会的问题怎么办？ 答：你可以先向老师抱歉，接着回答你的思路，或者对这个问题的看法，切记不要胡编乱造，大方承认自己不会。 有其他问题的欢迎留言，或者联系我的QQ：2013143650。赵阿卷欢迎你们来南师！ ::(太开心) 可以找我拿去年的复试笔试回忆试题和面试回忆试题。最后献上一张南师随园风景与大家共勉：
昨天在开发项目组项目时，有一个需求是将后端数据库返回的x、y点集渲染到前端。实际上是一个很简单的需求，使用前端的Canvas绘图就能完成，没想到调了大半天还没做出来，比例一直不对，遂放弃，回寝睡觉 ::(犀利) 。大概的意思就是像下面这种，绘图的比例会被拉伸：左边的图像就是昨天出现的“bug”，右侧的图像是正常的样子。在使用HTML5的Canvas元素时，内容或画布被拉伸的原因通常涉及到Canvas元素的大小与实际渲染尺寸之间的不匹配。今早顺着这条思路排查才发现Canvas绘图确实有个“坑点”。首先，Canvas元素会根据其属性中的宽度和高度进行初始化，Canvas 的默认大小为 300 像素 ×150 像素（宽 × 高，像素的单位是 px）。绘图操作会在这个初始化尺寸上进行，无论实际上屏幕上Canvas元素的显示尺寸是多少。绘制完成后，Canvas元素的内容会被放大或缩小到在CSS样式中设置的宽度和高度。如果CSS样式中的宽高与Canvas属性中的宽高不一致，就会导致内容在这个放大或缩小过程中发生变形。因为绘制是在Canvas属性指定的宽高上进行的，而放大或缩小是在绘制完成后的内容上进行的，所以这个过程可能导致图形的失真和比例不对。简单来说，如果在css中为canvas定义宽高，实际上把宽高为300px * 150px的画布进行了拉伸。应该在canvas属性中直接指定宽高。 或者也可以通过js直接指定宽高。const canvas = document.getElementById("myCanvas"); canvas.width=150; canvas.height=150;关于更多前端Canvas绘图教程可以参见MDN：https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial
项目组经过我和大师兄一个月的Coding，目前基本开发进度告一段落。可以沉下心来搞一搞机器学习相关的东西了。实际上，一开始进项目组的时候就想尝试采用机器学习（更具体的说是深度学习）的方式来做点不一样的事情，而不是纯数学的东西。当然，数学有数学的优势，机器学习有机器学习的优势。 扯得有点远，回到主题，这是最近学习CNN入门的时候练手用的，也应该算是我的第一个机器学习程序吧。记得做出来的时候，还是有点小小的激动，毕竟人生中第一次也不多，哈哈哈 ::(笑眼) 。中间也踩了不少坑，总结一下，整理成文，供后来者参考。CNN是什么 卷积神经网络（英语：convolutional neural network，缩写：CNN）是一种前馈神经网络，它的人工神经元可以响应一部分覆盖范围内的周围单元，对于大型图像处理有出色表现。 卷积神经网络由一个或多个卷积层和顶端的全连通层（对应经典的神经网络）组成，同时也包括关联权重和池化层（pooling layer）。这一结构使得卷积神经网络能够利用输入数据的二维结构。与其他深度学习结构相比，卷积神经网络在图像和语音识别方面能够给出更好的结果。这一模型也可以使用反向传播算法进行训练。相比较其他深度、前馈神经网络，卷积神经网络需要考量的参数更少，使之成为一种颇具吸引力的深度学习结构。 卷积神经网络的灵感来自于动物视觉皮层组织的神经连接方式。单个神经元只对有限区域内的刺激作出反应，不同神经元的感知区域相互重叠从而覆盖整个视野。 以上内容来自于维基百科 ，如果第一次接触的话，肯定是看的一脸懵。有机会我开个坑，尝试用大白话讲一讲。这里有个gif图，形象的展现了卷积的过程： MNIST数据集 MNIST数据集是一个手写数字识别数据集，常用于机器学习和深度学习的训练与测试。这个数据集包含了大量的手写数字图像，包括0到9的数字。这些图像都是28x28像素的灰度图像，每个图像都对应着一个标签，表示图像中所包含的数字。 MNIST手写数据集👉：官网 。官网中数据集一共分成了四个部分： 官网上提供了数据集的下载，主要包括四个文件：文件下载文件用途train-images-idx3-ubyte.gz训练集图像train-labels-idx1-ubyte.gz训练集标签t10k-images-idx3-ubyte.gz测试集图像t10k-labels-idx1-ubyte.gz测试集标签 在上述文件中，训练集一共包含了 60,000 张图像和标签，而测试集一共包含了 10,000 张图像和标签。测试集中前5000个来自最初NIST项目的训练集.，后5000个来自最初NIST项目的测试集。前5000个比后5000个要规整，这是因为前5000个数据来自于美国人口普查局的员工，而后5000个来自于大学生。 这里有一版整合了四个部分的数据集，推荐用这个，我已经上传至网盘，可以直接下载，我的代码也是基于这个整合后的数据集，下载解压即可✌️： 蓝奏云加载数据集 这里默认已经准备好所有环境，推荐使用Anaconda和PyCharm，新手友好。 加载数据集有两种方式，第一种是直接无需下载MNIST数据集，因为在Tensorflow中自带了，不过任然需要下载，且下载地址在海外，有可能会出现下载失败的情况，所以不推荐。代码如下，如果没有下载会自动下载：from keras.datasets import mnist (X_train, y_train), (X_test, y_test) = mnist.load_data() 下载的数据集路径位于：C:\Users\{用户名}\.keras。目前我还没有找到指定下载路径的方法，默认都放在了C盘下。 第二种方式是手动加载，把下载好的数据集文件放在项目目录下，编写加载数据集代码：# 加载Mnist数据集 def load_mnist(): with np.load('mnist.npz', allow_pickle=True) as data: return (data['x_train'], data['y_train']), (data['x_test'], data['y_test'])这里说明一下各个变量：x_train：训练集手写数据图像y_train：训练集手写数据图像对应的标签x_test：测试集手写数据图像y_test：测试集手写数据图像对应的标签我们可以通过下面函数的打印出形状：# 主函数 def main(): mnist = load_mnist() print_shape(mnist) # 打印形状 def print_shape(data): x_train, x_label = data[0] y_train, y_label = data[1] print(x_train.shape) print(x_label.shape) print(y_train.shape) print(y_label.shape)如果你和我一样，有这样的输出，那么就可以继续进行下一步了。预览数据集 上一步我们以及实现加载数据集，这里我们要看看这个数据集里的内容的真面貌。比如预览前25个图像，这里用到了matplotlib绘图库，如果没有安装，记得安装一下。import matplotlib.pyplot as plt # 主函数 def main(): mnist = load_mnist() view_image(mnist[0]) # 预览前25个图像 def view_image(data): x_train, y_train = data fig, ax = plt.subplots(nrows=5, ncols=5, sharex='all', sharey='all') ax = ax.flatten() for i in range(25): img = x_train[i].reshape(28, 28) ax[i].set_title(y_train[i]) ax[i].imshow(img, cmap='Greys', interpolation='nearest') ax[0].set_xticks([]) ax[0].set_yticks([]) plt.tight_layout() plt.show()预览结果：可以看到某些数字好像写的我自己都不认识，但是以标签为准，这也是机器学习的魅力。训练数据集并保存 这一步就是最核心的部分了。编写训练部分的代码，然后将训练好的模型保存下来，这样以后直接加载这个模型就可以了，不用再次训练。 由于MNIST数据集比较简单，我这里没有用到CNN卷积神经网络的卷积层和池化层，只进行了全连接层搭建。激活函数选择了relu，优化器选择Adam：# 训练并保存为SaveModel def train_model(mnist): x_train, y_train = mnist[0] x_test, y_test = mnist[1] # 归一化 x_train = x_train / 255.0 x_test = x_test / 255.0 model = keras.Sequential([ layers.Flatten(input_shape=(28, 28)), layers.Dense(128, activation='relu'), layers.Dense(10) ]) model.summary() model.compile(optimizer=keras.optimizers.Adam(), loss=losses.SparseCategoricalCrossentropy(from_logits=True), metrics=['accuracy']) model.fit(x_train, y_train, epochs=10) model.save('success') test_loss, test_acc = model.evaluate(x_test, y_test, verbose=2) print('\nTest accuracy:', test_acc) print('Test loss', test_loss) 等待10轮训练完成，可以看到loss在不断降低，accuracy在不断提高。最终可以达到97.7%的准确率 ::(真棒) 。 注意：这里都是通过CPU进行训练的，没有用到GPU，Tensorflow 2.10是Windows本地支持 GPU 的最后一个版本。从2.11开始，你就需要在WSL2上安装Tensorflow，或者使用TensorFlow-DirectML-Plugin。 我用的是2.15版本，不过好在这个数据集并不大，CPU已经完全够用了，整个过程不到20秒就训练结束。训练完成后，会把模型保存在项目文件夹下的success文件夹。加载模型并可视化验证结果 训练完毕后，我们可以加载保存好的模型，并进行验证。我这里就直接从测试集中随机选取10张图像来进行验证。有能力的小伙伴完全可以自己手写好数字，压缩成28*28的图像并用模型验证。# 加载模型并可视化验证结果 def load_model_check(mnist): matplotlib.rc("font", family='YouYuan') x_test1, y_test = mnist[1] # 归一化 x_test = x_test1 / 255.0 # 加载模型 model = keras.models.load_model('success') model.summary() plt.figure() for i in range(10): num = np.random.randint(1, 10000) plt.subplot(2, 5, i + 1) plt.axis('off') plt.imshow(x_test1[num], cmap='gray') demo = tf.reshape(x_test[num], (1, 28, 28)) y_pred = np.argmax(model.predict(demo)) plt.title('标签值：' + str(y_test[num]) + '\n预测值：' + str(y_pred)) plt.show() 注意，matplotlib库显示中文有可能会出现方框，记得指定一下字体（代码第二行），我这里就使用幼圆了。 可以看到预测结果还是很不错的，也给出了预测的时间，基本上都是在20ms范围内。总结 到此，Tensorflow2实现MNIST手写数据识别就结束了。有的人肯定会问我为啥不用pytorch，因为Tensorflow最大的优势是能够把模型迁移到各种位置，嵌入式、web都可以，而且还能够压缩模型，详情可看：https://tensorflow.google.cn/lite?hl=zh-cn。
开学一个多月了，踏入东方最美校园求学，是几年前的奢望，好在今年愿望成真😊。回想起七年前刚踏入大学的校门时，我根本没有想过有一天我会站在这个地方。这又是一段新的学习旅程，在这个新的阶段，我显得既兴奋又紧张，但我决定以积极的态度去面对。 这段时间充满了各种新奇和挑战，让我开启了学术之旅。刚开始的几天，我努力适应着研究生生活的节奏和规律。新的课表、新的教学方式、新的同学，一切都让我感到新鲜而又陌生。我记得第一次见导师的时候，开组会看到一群陌生的面孔，内心不免有些紧张。但随着时间的推移，我发现自己渐渐地融入了这个环境。 9月4号那天，我收到了一份特殊的请柬，高中时代的好友即将步入婚姻的殿堂。这个消息让我激动又感慨，因为能够亲眼见证好友幸福的时刻是一种难以言喻的喜悦。这也是我第一次参加同学的婚礼。 由于国庆期间项目组需要留校做开发，10月2号需要回来，于是就匆匆的买了9月30号的高铁到苏州去参加婚礼。到达苏州后，见到很多老同学。熟悉的面孔一一浮现在眼前，仿佛回到了高中时代的校园。大家都成长了许多，有的创业有成，有的还在读书，还有的在国企央企上班，我们的年龄虽然都在变大，但内心的善意和深情却没有改变，这种真挚的友谊让我感动不已。 饭后，我们也共同回忆起了高中时代的点点滴滴。那些一起上课、一起放学、一起度过难忘时光的回忆如电影般在我脑海中闪现。我们互相分享了这几年各自的生活经历，谈论着即将面临的未来，一时间仿佛又回到了曾经的青春岁月。 有那么一瞬间让我明白了友谊的珍贵和时光的流转。无论是研究生生涯还是友情的延续，都需要我们珍惜和用心去对待。 回顾过去一个多月的研究生生活，我明白到成长并不是一帆风顺的，但正是这些经历和挑战，让我变得更加坚强和自信。我期待着接下来的日子，愿意积极投身于学习和研究中，迎接未知的挑战。 或许，未来的日子里还会有更多的喜庆和欢乐，也许还会有更多的同学结婚、升学、就业，但无论发生什么，我都希望能够保持这份真情实感，用心记录下每一个重要的时刻，将它们编织成属于我的独特篇章。研究生生活的每一天都是一次成长和收获，我相信，在这个全新的阶段，我会敞开心扉，迎接更多精彩的人和事，书写属于自己的不凡人生。