-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.json
1 lines (1 loc) · 120 KB
/
index.json
1
[{"content":"最近在进行一些 Android app 蓝牙相关的开发,需要前台长时间高频率的扫描 BLE 设备,使用了 Android 提供的 SCAN_MODE_LOW_LATENCY 扫描模式,扫描 30 分钟后出现了扫描不到 BLE 设备的情况。\n问题 使用自己开发的 Android app ,蓝牙扫描代码为:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import android.bluetooth.le.BluetoothLeScanner; import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanSettings; public class BluetoothScanManager { private static BluetoothLeScanner bluetoothLeScanner; private static ScanCallback scanCallback; private static ScanSettings scanSettings; public BluetoothScanManager(BluetoothLeScanner bluetoothLeScanner, ScanCallback scanCallback) { BluetoothScanManager.bluetoothLeScanner = bluetoothLeScanner; BluetoothScanManager.scanCallback = scanCallback; BluetoothScanManager.scanSettings = new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) .build(); } private static void startScan() { bluetoothLeScanner.startScan(null, scanSettings, scanCallback); } private static void stopScan() { bluetoothLeScanner.stopScan(scanCallback); } } 使用的测试手机的 Android 版本为 7.0 。程序开始扫描的前 30 分钟可以正常的高频扫描到 BLE 设备,30 分钟后一个 BLE 设备也扫描不到了。\n查找原因 根据搜索引擎检索到的相关信息查看 Android 蓝牙相关源码,在 180 行找到了处理 BLE 扫描操作的类:\n1 2 // Handler class that handles BLE scan operations. private class ClientHandler extends Handler { 其中用来接收消息的函数为:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Override public void handleMessage(Message msg) { ScanClient client = (ScanClient) msg.obj; switch (msg.what) { case MSG_START_BLE_SCAN: handleStartScan(client); break; case MSG_STOP_BLE_SCAN: handleStopScan(client); break; case MSG_FLUSH_BATCH_RESULTS: handleFlushBatchResults(client); break; case MSG_SCAN_TIMEOUT: mScanNative.regularScanTimeout(); break; default: // Shouldn\u0026#39;t happen. Log.e(TAG, \u0026#34;received an unkown message : \u0026#34; + msg.what); } } 当收到开始 BLE 扫描的信号后会执行 handleStartScan 函数:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 void handleStartScan(ScanClient client) { Utils.enforceAdminPermission(mService); logd(\u0026#34;handling starting scan\u0026#34;); if (!isScanSupported(client)) { Log.e(TAG, \u0026#34;Scan settings not supported\u0026#34;); return; } if (mRegularScanClients.contains(client) || mBatchClients.contains(client)) { Log.e(TAG, \u0026#34;Scan already started\u0026#34;); return; } // Begin scan operations. if (isBatchClient(client)) { mBatchClients.add(client); mScanNative.startBatchScan(client); } else { mRegularScanClients.add(client); mScanNative.startRegularScan(client); if (!mScanNative.isOpportunisticScanClient(client)) { mScanNative.configureRegularScanParams(); if (!mScanNative.isFirstMatchScanClient(client)) { Message msg = mHandler.obtainMessage(MSG_SCAN_TIMEOUT); msg.obj = client; // Only one timeout message should exist at any time mHandler.removeMessages(SCAN_TIMEOUT_MS); mHandler.sendMessageDelayed(msg, SCAN_TIMEOUT_MS); } } // Update BatteryStats with this workload. try { mBatteryStats.noteBleScanStarted(client.workSource); } catch (RemoteException e) { /* ignore */ } } } 其中重要的是 // Begin scan operations 的 else 代码块中的第二个 if 代码块 ,首先会进行一个 isFirstMatchScanClient 函数的判断。函数在 531 行:\n1 2 3 private boolean isFirstMatchScanClient(ScanClient client) { return (client.settings.getCallbackType() \u0026amp; ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0; } 这个函数会将 client 的 CallbackType 和 ScanSettings.CALLBACK_TYPE_FIRST_MATCH 进行比对看是否一致,不一致的话会创建一个超时消息并使用 sendMessageDelayed 函数延迟 SCAN_TIMEOUT_MS 后发送。SCAN_TIMEOUT_MS 的值在 72 行:\n1 2 // Maximum msec before scan gets downgraded to opportunistic private static final int SCAN_TIMEOUT_MS = 30 * 60 * 1000; 这个值就和遇到的 30 分钟后扫描不到 BLE 设备对上了。这个延迟 30 分钟后发送的消息会触发上面的 handleMessage 中的\n1 2 3 case MSG_SCAN_TIMEOUT: mScanNative.regularScanTimeout(); break; regularScanTimeout 函数在 674 行:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void regularScanTimeout() { for (ScanClient client : mRegularScanClients) { if (!isOpportunisticScanClient(client) \u0026amp;\u0026amp; !isFirstMatchScanClient(client)) { logd(\u0026#34;clientIf set to scan opportunisticly: \u0026#34; + client.clientIf); setOpportunisticScanClient(client); client.stats.setScanTimeout(); } } // The scan should continue for background scans configureRegularScanParams(); if (numRegularScanClients() == 0) { logd(\u0026#34;stop scan\u0026#34;); gattClientScanNative(false); } } void setOpportunisticScanClient(ScanClient client) { // TODO: Add constructor to ScanSettings.Builder // that can copy values from an existing ScanSettings object ScanSettings.Builder builder = new ScanSettings.Builder(); ScanSettings settings = client.settings; builder.setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC); builder.setCallbackType(settings.getCallbackType()); builder.setScanResultType(settings.getScanResultType()); builder.setReportDelay(settings.getReportDelayMillis()); builder.setNumOfMatches(settings.getNumOfMatches()); client.settings = builder.build(); } 可以看到 regularScanTimeout 会把我们的蓝牙扫描模式设置为 ScanSettings.SCAN_MODE_OPPORTUNISTIC 。Android developer 的文档对这个扫描模式的描述为:\nA special Bluetooth LE scan mode. Applications using this scan mode will passively listen for other scan results without starting BLE scans themselves.\n根据文档,这种模式仅在其他应用已经启用了蓝牙扫描的情况下才会工作。也就是说,扫描不会主动进行,只会依赖其他应用的扫描行为。\n至此,问题基本查清了。总结一下,当我们设置的 BLE 扫描的 CallbackType 不为 ScanSettings.CALLBACK_TYPE_FIRST_MATCH 时,BLE 的 ScanManager 会自动为我们 BLE 扫描的 client 增加一个 30 分钟后的超时消息发送,这个超时消息会改变我们的蓝牙扫描模式并把我们 BLE 扫描的 client 的状态设置为超时,导致我们搜索不到 BLE 设备。\n解决方式 由于这个 30 分钟的限制只针对我们 BLE 扫描的 client 。所以我们可以在开始扫描的同时创建一个定时任务,在一段时间(\u0026lt;30 分钟),比如 29 分钟后停止蓝牙扫描,等待一秒,然后再开始蓝牙扫描。大概代码为:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import android.bluetooth.le.BluetoothLeScanner; import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanSettings; import android.os.Handler; import android.os.Looper; public class BluetoothScanManager { private static BluetoothLeScanner bluetoothLeScanner; private static ScanCallback scanCallback; private static ScanSettings scanSettings; private static Handler handler; private static final long SCAN_DURATION = 29 * 60 * 1000; // 29 minutes in milliseconds public BluetoothScanManager(BluetoothLeScanner bluetoothLeScanner, ScanCallback scanCallback) { BluetoothScanManager.bluetoothLeScanner = bluetoothLeScanner; BluetoothScanManager.scanCallback = scanCallback; BluetoothScanManager.handler = new Handler(Looper.getMainLooper()); BluetoothScanManager.scanSettings = new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build(); } private static void startScan() { bluetoothLeScanner.startScan(null, scanSettings, scanCallback); handler.postDelayed(() -\u0026gt; { stopScan(); handler.postDelayed(BluetoothScanManager::startScan, 1000); // Wait 1 second before restarting scan }, SCAN_DURATION); } private static void stopScan() { bluetoothLeScanner.stopScan(scanCallback); } } 修改后测试了两个小时,可以不间断的搜索到 BLE 设备。具体的 SCAN_DURATION 可能会根据不同厂商或不同安卓版本来修改。目前最新的 main 分支上的相关代码做了修改,但这个参数的值并没有变,详见 AdapterService.java 。\n为什么会有这个限制 AOSP code review system 上增加这个限制的 commit 消息为:\nAdd protection against LE scanning abuse\nAdded two checks to prevent abuse. The first check ensures that an app doesn\u0026rsquo;t scan too frequently in a certain time period. It is allowed to scan again after its oldest scan exceedes said time period. The second check ensures that an app doesn\u0026rsquo;t scan for too long. Upon starting a scan, this code waits a certain amount of time. If the app is still scanning by that point, this code stops the scan and forces the app to use opportunistic scanning instead.\n看来是为了防止 BLE 扫描的滥用。而且一开始的超时时间设置的为 5 分钟,后边的一次提交才改为 30 分钟。但这些限制并没有在开发者文档中相关的地方标注出来。\n参考链接 https://juejin.cn/post/7046328465108402207 https://blog.classycode.com/undocumented-android-7-ble-behavior-changes-d1a9bd87d983 ","permalink":"https://blog.raikiriww.net/post/08c770aea2ea8fda/","summary":"\u003cp\u003e最近在进行一些 Android app 蓝牙相关的开发,需要前台长时间高频率的扫描 BLE 设备,使用了 Android 提供的 \u003ccode\u003eSCAN_MODE_LOW_LATENCY\u003c/code\u003e 扫描模式,扫描 30 分钟后出现了扫描不到 BLE 设备的情况。\u003c/p\u003e","title":"Android BLE 长时间高频率扫描"},{"content":"最近在自己家中的迷你主机上部署了一些自己用的服务,使用 frp 做内网穿透将迷你主机的服务映射到公网供自己使用。frp 支持 SSH 的端口复用,但需要使用到 Socat 这个工具。Socat 在 linux 和 mac 环境下都可以使用命令直接下载使用,但 windows 没有现成的库,网上找到的都是别人自己编译,也不知道靠不靠谱,所以决定使用 Github action 全自动,透明的编译一版 windows 能直接用的 Socat 。目前已经做好了,项目地址:https://github.com/raikiriww/socat_windows 。\n项目介绍 整个项目目前(2024-03-03)只有一个 readme 和 Github action 的执行文件。具体为:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 name: Cygwin Build on: # Triggers the workflow on push or pull request events but only for the \u0026#34;main\u0026#34; branch push: branches: [ \u0026#34;main\u0026#34; ] pull_request: branches: [ \u0026#34;main\u0026#34; ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: jobs: build: permissions: write-all runs-on: windows-latest steps: - uses: actions/checkout@v2 - name: Install Cygwin run: | choco install cygwin choco install cyg-get cyg-get gcc-g++ gcc-core cygwin32-gcc-g++ cygwin32-gcc-core make gcc-fortran gcc-objc gcc-objc++ libkrb5-devel libkrb5_3 libreadline-devel libssl-devel libwrap-devel tcp_wrappers - name: Configure Environment run: | echo \u0026#34;C:\\tools\\cygwin\\bin\u0026#34; \u0026gt;\u0026gt; $GITHUB_PATH - name: Download and Extract the archive run: | curl -O http://www.dest-unreach.org/socat/download/socat-1.8.0.0.tar.gz tar -xvzf socat-1.8.0.0.tar.gz - name: Execute Build Script in Cygwin run: | C:\\tools\\cygwin\\bin\\bash -lc \u0026#34;cd /cygdrive/d/a/socat_windows/socat_windows/socat-1.8.0.0 \u0026amp;\u0026amp; ./configure \u0026amp;\u0026amp; make \u0026amp;\u0026amp; make install\u0026#34; - name: Copy Cygwin DLLs run: | Copy-Item \u0026#34;C:\\tools\\cygwin\\bin\\cygcrypto-3.dll\u0026#34; -Destination \u0026#34;D:\\a\\socat_windows\\socat_windows\\socat-1.8.0.0\u0026#34; Copy-Item \u0026#34;C:\\tools\\cygwin\\bin\\cygwin1.dll\u0026#34; -Destination \u0026#34;D:\\a\\socat_windows\\socat_windows\\socat-1.8.0.0\u0026#34; Copy-Item \u0026#34;C:\\tools\\cygwin\\bin\\cygssl-3.dll\u0026#34; -Destination \u0026#34;D:\\a\\socat_windows\\socat_windows\\socat-1.8.0.0\u0026#34; Copy-Item \u0026#34;C:\\tools\\cygwin\\bin\\cygreadline7.dll\u0026#34; -Destination \u0026#34;D:\\a\\socat_windows\\socat_windows\\socat-1.8.0.0\u0026#34; Copy-Item \u0026#34;C:\\tools\\cygwin\\bin\\cygwrap-0.dll\u0026#34; -Destination \u0026#34;D:\\a\\socat_windows\\socat_windows\\socat-1.8.0.0\u0026#34; Copy-Item \u0026#34;C:\\tools\\cygwin\\bin\\cygncursesw-10.dll\u0026#34; -Destination \u0026#34;D:\\a\\socat_windows\\socat_windows\\socat-1.8.0.0\u0026#34; Copy-Item \u0026#34;C:\\tools\\cygwin\\bin\\cygz.dll\u0026#34; -Destination \u0026#34;D:\\a\\socat_windows\\socat_windows\\socat-1.8.0.0\u0026#34; shell: pwsh - name: Upload Artifacts if: ${{ success() }} uses: actions/upload-artifact@v2 with: name: socat-1.8.0.0 path: socat-1.8.0.0/ - name: Archive production artifacts if: ${{ success() }} run: 7z a socat-1.8.0.0.zip D:\\a\\socat_windows\\socat_windows\\socat-1.8.0.0 - name: Upload Release uses: softprops/action-gh-release@v1 if: ${{ success() }} with: tag_name: 1.8.0.0 files: socat-1.8.0.0.zip 整个编译过程在 Github 的 windows runner 上执行。使用 Cygwin 作为编译工具。编译过程分为下面几步:\n初始化代码仓库。这步会将仓库的所有文件下载到 runner 中。 安装 Cygwin 和依赖的库。 将 Cygwin 的 bin 目录加到环境变量中。 下载 Socat 的源码。 使用 Cygwin 编译。 拷贝依赖的 dll 文件到编译好的程序目录。 打包编译好的程序作为 Artifact 。 将打包好的压缩包作为 Release 发布。 使用方法 下载项目 Releases 页面的程序压缩包。解压到本地。 将解压好的文件夹路径添加到 windows 的 PATH 环境变量中。 打开终端输入 socat 即可使用。编译好的程序在我自己的电脑上(windows11 22H2 22621.2506)运行良好。\n","permalink":"https://blog.raikiriww.net/post/7f48681db0ca9a86/","summary":"\u003cp\u003e最近在自己家中的迷你主机上部署了一些自己用的服务,使用 frp 做内网穿透将迷你主机的服务映射到公网供自己使用。frp 支持 SSH 的端口复用,但需要使用到 Socat 这个工具。Socat 在 linux 和 mac 环境下都可以使用命令直接下载使用,但 windows 没有现成的库,网上找到的都是别人自己编译,也不知道靠不靠谱,所以决定使用 Github action 全自动,透明的编译一版 windows 能直接用的 Socat 。目前已经做好了,项目地址:\u003ca href=\"https://github.com/raikiriww/socat_windows\"\u003ehttps://github.com/raikiriww/socat_windows\u003c/a\u003e 。\u003c/p\u003e","title":"Windows 系统使用 Socat"},{"content":"非监督学习\n聚类(Clustering) 聚类是一种非监督学习算法,可以自动寻找数据中的结构,将相似的数据自动归为一组。相比于分类算法,它不需要提供训练数据的真值 y 。聚类算法的应用有:\n合并同类新闻 市场划分 DNA 分析 天文数据分析 K均值聚类算法(K-mean algorithm) K 均值聚类算法是聚类算法的一种。它会初始化 K 个聚类中心(cluster centroids),然后对所有数据重复下面两步操作:\n求每个数据到所有聚类中心的距离,找到距离最小的那个,把这个数据分配给这个聚类中心 计算每个聚类中心的所有数据的均值,将这个均值指定为每个聚类中心新的位置。 重复上面两步直至聚类中心的位置不再变化。\n有特殊情况是有些聚类中心没有分配到数据。常见的做法是去除这个聚类中心,或者也可以重新初始化这个聚类中心,期待它重新计算后可以分配到数据。\n代价函数 第二步中我们会计算每个聚类中心与其被分配到的数据的距离,将这个距离平方,将所有距离的平方相加然后除以数据总数,我们就得到了 K 均值算法的代价函数。也被称为 Distortion 。上面一直重复的两步操作都在最小化这个代价函数。当代价停止减小时,我们就可以停止重复上面的两步并检查模型的拟合情况。\n初始值选择 假设我们有 K 个聚类中心,一般的做法是从所有数据中随机选择 K 个数据,将它们作为 K 个聚类中心的初始值。但单次随机选择容易得到几个相同的聚类中心,达不到最好的效果。所以,我们会选择 50-1000 次 K 个初始值,然后对这么多个初始值分别应用聚类算法,但每个初始值只计算一次代价,并不重复上面提到的两步操作。然后,从所有这些代价中选择最小的那个作为我们真正的初始值来重复上面的两步。\nK 值选择 选择一个合适的聚类中心的数量会让我们的 K 值聚类算法拟合的更好。但很多场景并没有一个明显的 K 值。一个方法是尝试多个不同的 K 值,画出 K 值和代价的曲线,找到拐点,也就是代价下降由快到慢的那个转折点的 K 值作为实际使用的 K 值。在有些情况下代价随着 K 值的增加是缓慢下降的,这样这个方法就无法确定 K 值了。\n实际上我们训练 K聚类算法模型是为了后续的用途,所以,我们要根据后续实际用途的效果来权衡 K 值的选择。比如:我们训练一个 K 均值聚类算法模型来压缩图片,根据我们想要压缩后的图片是比较清晰还是比较小,我们可以需要不同的 K 值。\n异常检测(Anomaly detection) 异常检测是不同于聚类的另一种算法,它用于检测异常的行为。比如:制造出的物品是否合格,用户在网站上的行为是否正常,是否被盗号。\n实现异常检测算法的流程为:\n选择 \\( n \\) 个与异常行为有关的特征 \\( x \\) 在每个特征上计算平均数和方差 在新的样本上根据平均数和方差根据高斯分布计算每个特征的概率并相乘得到一个概率 将得到的概率与一个阈值比较,小于阈值会判定为异常行为 总体来看,异常检测算法是在检测是否有一个或多个特征出现的概率很小,小就判定为异常。\n模型评估和优化 当我们训练好一个异常检测算法模型后,我们怎么选择一个好的阈值呢?怎么去优化这个模型呢?这时候就需要进行模型的评估。虽然异常检测是非监督学习算法,但那是指训练的时候我们没有用到有标签的数据,在评估过程中我们可以找一些确认为异常的数据,将他们和部分正常数据混合成两部分:交叉验证集和测试集,这点和监督学习是一样的。我们可以在交叉验证集中评估我们的模型,根据结果调整这个阈值,然后将最终的模型在测试集上测试并得到我们模型最终的效果。\n当我们用于评估的异常数据样本很少时,比如只有一两个,那我们可以部分成交叉验证集和测试集,而是将全部的异常样本和部分正常样本混合成交叉验证集,不要测试集。这也可以优化我们的模型,但有过拟合的风险。\n异常检测与监督学习 之前提到,我们可以用一些有标签的数据来优化和评估我们的模型,那我们为什么不直接训练一个监督学习模型呢?🤔下面是异常检测和监督学习模式合适应用场景的比较:\n异常检测:\n当异常数据的量很少,正常数据的量很大时 当异常情况可能会与我们拥有的样本完全不同时 监督学习模型:\n当异常数据和正常数据的数量都很多时 当未来出来的异常情况都与我们的样本相似时 造成这些不同适用条件的原因是:\n异常检测算法是计算数据是正常数据的概率,我们的模型在大量的正常数据样本上总结了经验,当新的数据与我们正常数据相差很多时,不管它是否出现在我们评估时用到的异常数据中,它都会被模型识别到出现的概率很小并判断为异常数据。比如诈骗,诈骗招数会与时俱进,我们的诈骗样本可能过几年就不适用了。 监督学习模型是既总结正常数据的经验,又总结异常数据的经验。所以,当我们的异常数据很少或我们的异常数据样本并没有包含大多数的异常情况时,监督学习模型的效果会很差。当我们有足够多的异常和正常数据时,它会表现的很好。比如垃圾邮件,垃圾邮件虽然有很多种,但基本都是相同的情况,要卖给你东西或者诱导你点击网站。 特征选择 在监督学习中,模型有数据的标签作为真值,它可以过滤掉没用的特征或调整调整的参数来适应特征。而在非监督学习中,我们没有真值给模型训练,所以,选择合适的特征尤为重要。\n高斯化 我们计算每个特征的概率时是根据高斯分布来计算的,加入我们原本特征的分布不是高斯分布,那我们计算的概率可能不会很好。所以,我们需要绘制每个特征的分布曲线,如果不是高斯分布,那就使用一些函数来将该特征转换为类似高斯分布的数据。比如使用 \\( \\log\\left( x + C \\right) \\) ,取平方根,立方根等来将原来的数据转为新的数据。\n错误分析 当我们使用了交叉验证集评估了模型后,我们可以查看模型预测错的例子,找出模型为什么预测错了,尝试一些办法,比如增加新的特征来使模型可以正确预测这种数据。\n创建新的特征 有时,异常发生时,单个特征值会很大,但平常这个特征的值可能会和其他特征值一起变得很大,这时候就需要我们将几个特征结合起来组成新的特征。比如,正常服务器运行时 CPU 和网络 IO 都可能很大,当被黑客入侵时, CPU 负载可能会很大,网络 IO 不大。这时,我们就可以将 CPU 负载和网络 IO 结合起来组成新的特征:CPU负载/网络 IO。这样就可以区分这两种情况。\n推荐系统 推荐系统在现实世界中的应用非常广泛,当我们访问例如淘宝,京东之类的网站,它们都会根据我们的喜好来推荐不同的商品。\n协同过滤(Collaborative Filtering) 协同过滤算法的目的是生成两个向量,假设我们要针对电影来做推荐系统。那么我们生成的两个向量为:\n对于每个用户,生成能够表现这个用户电影品味(喜欢程度)的向量 对于每个电影,生成能够代表这个电影特征的向量 这两个向量的点乘加上一个偏差值就代表我们的算法预测该用户会对该电影打多少分。\n代价函数 $$J({\\mathbf{x}^{(0)},...,\\mathbf{x}^{(n_m-1)},\\mathbf{w}^{(0)},b^{(0)},...,\\mathbf{w}^{(n_u-1)},b^{(n_u-1)}})= \\left[ \\frac{1}{2}\\sum_{(i,j):r(i,j)=1}(\\mathbf{w}^{(j)} \\cdot \\mathbf{x}^{(i)} + b^{(j)} - y^{(i,j)})^2 \\right] + \\underbrace{\\left[\\frac{\\lambda}{2}\\sum_{j=0}^{n_u-1}\\sum_{k=0}^{n-1}(\\mathbf{w}^{(j)}_k)^2 + \\frac{\\lambda}{2}\\sum_{i=0}^{n_m-1}\\sum_{k=0}^{n-1}(\\mathbf{x}_k^{(i)})^2\\right]}_{regularization}$$\n其实协同过滤的代价函数和线性回归差不多,主要的区别是变量不仅是 \\(w\\) 和 \\(b\\),还有 \\(x\\)。我们会同时训练 \\(w\\), \\(x\\), \\(b\\)\n$$ w = w - \\alpha \\frac{\\partial J(w,b,x)}{\\partial w} $$\n$$ b = b - \\alpha \\frac{\\partial J(w,b,x)}{\\partial b} $$\n$$ x = x - \\alpha \\frac{\\partial J(w,b,x)}{\\partial x} $$\n当我们的训练数据是二元的标签数据时,例如:喜欢/不喜欢。我们可以将 \\(\\mathbf{w}^{(j)} \\cdot \\mathbf{x}^{(i)} + b^{(j)}\\) 通过 sigmod 函数处理,然后代价函数就变成了类似逻辑回归的代价函数。\n均值归一化(Mean normalization) 当我们训练好协同过滤模型后,如果有一个新用户,他没有评价过任何一部电影,当我们初始的 \\(b\\) 为 0 时,那么我们的模型将会预测它会给所有电影打 0 分,这显然是不对的。我们可以用均值归一化来解决这个问题。\n还是以电影评分为例。我们可以取每个电影的均分,然后将每个用户的评分减去这个均分。当我们计算这个用户对某个电影的评分时,我们需要再加上这个均分,也就是从 \\(\\mathbf{w}^{(j)} \\cdot \\mathbf{x}^{(i)} + b^{(j)}\\) 改为 \\(\\mathbf{w}^{(j)} \\cdot \\mathbf{x}^{(i)} + b^{(j)} + u\\) 。 \\(u\\) 指的就是这个均分。这样,当模型预测一个从没有评价过电影的新用户对某个电影的评分时,这个评分会时这个电影的平均评分,比之前的 0 合理。\nTensorFlow 实现 我们需要使用 TensorFlow 的 Custom Training Loop 功能来实现我们的算法。Custom Training Loop 可以让我们子集自定义计算代价的公式,TensorFlow 会自动帮助我们根据公式求偏导数,这一特性叫做 Auto Diff 。示例代码为:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 optimizer = keras.optimizers.Adam(learning_rate=1e-1) iterations = 200 lambda_ = 1 for iter in range(iterations): # Use TensorFlow’s GradientTape # to record the operations used to compute the cost with tf.GradientTape() as tape: # Compute the cost (forward pass included in cost) cost_value = cofi_cost_func_v(X, W, b, Ynorm, R, lambda_) # Use the gradient tape to automatically retrieve # the gradients of the trainable variables with respect to the loss grads = tape.gradient( cost_value, [X,W,b] ) # Run one step of gradient descent by updating # the value of the variables to minimize the loss. optimizer.apply_gradients( zip(grads, [X,W,b]) ) 找到相关的物品 当我们训练好协同过滤模型后,我们会得到每个物品的特征向量。当我们需要找到与某个物品相关的物品时,我们可以将所有的特征向量与该物品的的特征向量做差值并。找到差值最小的即可。\n局限性 冷启动问题:当有一个全新的,没有人评过分的物品时,协同过滤算法的效果不好。当用户评过分的物品较少时,协同过滤算法的效果也不好。 对附带信息的使用不是很好:例如用户的性别,年龄,位置。物品的类别,流派等。 内容过滤(Content-based filtering) 相比于协同过滤,我们有用户的特征(年龄,性别等)和物品的特征(流派,平均评分等)。我们想基于这些特征来给用户推荐合适的物品。我们要做的就是计算用户的特征向量 v_u 和物品的特征向量 v_m ,并将这两个向量点乘来判断该物品和用户是否合适。\n理论实现 我们可以构建两个神经网络:用户神经网络和物品神经网络来分别从用户和物品的特征中生成对应的特征向量。因为我们最终要点乘这两个神经网络的输出,所以这两个神经网络拥有相同结构的输出层,隐藏层可以不同。然后将这两个神经网络合并成一个来作为我们最终的神经网络。当我们想要的结果是评估用户是否想要这个物品时,我们可以将点乘的结果套上一个 sigmod 函数来使其输出 0/1 。\n性能优化 内容过滤推荐系统的运行方式可以分为两步:检索和排序\n检索:检索的功能是生成一个用户可能会喜欢的物品的列表。例如:\n查看用户最新浏览的十个物品,找到十个相似的物品加入列表 统计用户浏览过的物品,找到出现率最高的三个类别,将这些类别的 top 10 物品加入列表 将用户所属国家的 top 30 加入列表 将上述所有步骤生成的列表整合到一个列表中,去除重复的和用户已经有的。\n排序:将检索生成的列表中的所有物品和用户特征放入神经网络进行推理,得到用户对这些物品的可能喜爱程度并按照从大到小的顺序进行排序再展示给用户。\n这之中存在一个权衡,当我们检索功能输出的列表很大时,我们根据这个列表排序就会很慢。当我们降低检索功能输出的列表大小时,我们又担心检索范围太小,无法检索到用户真正喜欢的物品。所以,我们需要进行离线测试,尝试不同的检索功能输出的列表大小来找到一个满足我们需求的点。\n道德问题 构建推荐系统时,应考虑在道德方面的影响。尽量构建符合道德的,对社会和人类有益的系统。\n主成分分析(Principal Component Analysis) 当我们有一个比较复杂的数据集,它拥有十几个或者成百上千个特征时,我们很难去可视化这样的数据集。主成分分析可以帮助我们将大量的特征转化为两个或三个特征来方便我们可视化数据。比如:它可以删除变化不大的特征,将某几个相关特征整合成一个等。这样,我们就可以通过可视化更好的理解数据。\n具体做法 假设我们有一组数据,x1 作为横轴, x2 作为纵轴。如果他们的取值范围相差很大,需要先进行特征缩放。接下来,PCA 算法会尝试找到一个新的轴,从每个点到这个新的轴作垂线,相交的点就看作该点在这个新的轴上的映射。然后计算所有点在这个轴上的方差,这时的方差可以看作保留原本信息的程度,越大越好。重复上述步骤,找到方差最大的那个轴作为原来的 x1 和 x2 的替代。比如一个有一个点(2, 3)。原来的 x1, x2 两个轴的(1,1)在新的轴上的坐标为(0.7, 0.7)那么这个点(2, 3)在新的轴上的坐标就是 2 * 0.7 + 3 * 0.7 = 3.5 。这样确定好第一个轴后,第二个轴就是跟第一个轴垂直的那条直线,第三个轴就是和第一,第二轴组成的平面垂直的那条直线。\n与线性回归的区别 看起来主成分分析的做法与线性回归差不多,都是拟合一条直线。但它们还是有区别的。\n线性回归计算的是每个点的 y 与拟合的直线的距离,这个距离是从该点做一条垂直于 x 轴的垂线与拟合的直线相交,取这条线段的距离。线性回归使用 y 的距离作为调整拟合的直线的依据。 主成分分析是从 x1, x2 这个点做一条垂直于拟合的轴的垂线,取这个线段作为距离。它是使用 x1, x2 两个特征作为调整轴的依据的。 反推原来点的坐标 在我们使用新的轴替代了老的两个轴后,我们已经不可能通过新的轴上的点的坐标反推出旧的点的精确坐标的。但我们可以得到一个近似值,将新的坐标轴的点乘上原来坐标轴的(1,1)点在新的轴上所对应的坐标就行。比如 3.5 = (3.5 * 0.7, 3.5 * 0.7) = (2.45, 2.45)。相较于原来的(2,3)可以看作一个近似值。\n强化学习 当我们想要使用代码自动控制一个机器人或者飞行器时,我们有这个物体的状态(坐标,动力大小,方向等),想要训练一个监督学习模型来根据这些状态来自动决定下一步干什么是很困难的,因为正确的下一步动作是模棱两可的,你可以加油门,也可以调整倾角。这时,我们需要强化学习来解决这个事。\n强化学习的关键是奖励函数(reward function) ,这个函数可以判断算法模型的输出是否是好的,满足我们期望的输出。如果是,那就奖励算法,如果不是,那就惩罚算法。通过这个奖励函数来让算法自己发现怎么做才能取得好的结果。强化学习与其他算法的区别就在这里,我们不会教算法怎么去取得好的结果,而是评判它每次做的好不好从而让算法自己找到方法去取得好的结果。有点像训练狗狗,当狗狗表现的好时,我们就奖励狗狗吃的或者夸它,当狗狗表现的不好时,我们就批评它。通过奖惩来训练它称为满足我们要求的好狗狗🐶。\n为了实现这个奖励机制,我们需要计算模型每个动作能够获得的回报(return),这个回报可以为正数,零或者负数。每进行一个动作,我们会将该动作获得的奖励乘上一个折扣系数(discount factor)的 n 次方,n 为步数。比如:一开始的奖励为 0 ,我们第一个动作做完后,本应获得的奖励为 10 ,我们设定的折扣系数为 0.9 。那么实际获得的奖励就是 10 * 0.9 = 9 。接下来我们执行第二个动作,第二个动作本来给的奖励为 20 ,乘上折扣系数,实际的奖励为 20 * (0.9 ** 2) = 16.2 。假设第二个动作完成后我们达到了结束状态(terminal state)。那么,我们总共获得的奖励为 0 + 9 + 16.2 = 25.2 。\n我们还需要一个策略(policy)\\(\\pi\\) 来将状态与实际行动通过上面提到的奖励函数结合起来。我们给这个策略一个确定的状态,策略可以根据这个状态计算接下来的回报,比较不同的回报来输出一个行动,然后实际执行这个行动。\\(action = \\pi(state) \\)\n总结一下,我们上面提到的所有步骤整合起来就是强化学习的运作形式,这个形式有一个名字:马尔可夫决策过程(Markov Decision Process) ,简称 MDP 。马尔可夫指的是:未来的状态仅取决于现在的状态,与当前状态的之前的所有事都无关。\n状态价值函数(State-action value function)(Q-function) 状态价值函数,简称 Q 函数,这个函数用于帮助我们计算当前状态 s 时,我们采取行动 a 能取得的回报是多少。通过在当前状态尝试多种不同的 a ,取其中回报最大的 a 来当作我们当前状态的 \\( \\pi(state)\\) 所输出的 a 。\n贝尔曼方程(Bellman Equation) $$ Q(s, a) = R(s) + \\gamma \\max_{a\u0026rsquo;} Q(s\u0026rsquo;, a\u0026rsquo;) $$\n上面就是贝尔曼方程,它就是我们用来实现状态价值函数的方法。其中,\\(R(s)\\) 是当前状态的回报,\\(\\gamma\\) 是折扣系数,\\(s\u0026rsquo;\\) 是我们执行完动作 a 后进入到的下一个状态,\\(a\u0026rsquo;\\) 是 \\(s\u0026rsquo;\\) 状态所能执行的动作。方程等号的右边可以分为两部分:\n\\(R(s)\\) 代表即时奖励 \\( \\gamma \\max_{a\u0026rsquo;} Q(s\u0026rsquo;, a\u0026rsquo;)\\) 代表后续获得的奖励 神经网络架构 我们可以使用神经网络来实现对函数 \\(Q(s, a)\\) 的模拟。神经网络的输入为当前的状态 s ,由多个参数构成,例如x, y, v_x, x_y 等。输出层的神经元个数等同于所有的动作个数。假设我们对于每个状态都有三个动作可以选择:向左,向右,什么也不做。那么我们要构建的神经网络的输出层的神经元个数就是 3 。这样可以一次推理就计算出所有可能动作的回报,使我们的计算更有效率。\n完整的过程为:\n随机初始化神经网络的参数 通过实验收集各种各样的所需数据:当前状态 s,选择的动作 a,当前回报 R(s),执行完动作所进入到的下一个状态 s\u0026rsquo;。存储最新的大概 10000 个数据 然后将每组数据的 s 作为 x ,\\(R(s) + \\gamma \\max_{a\u0026rsquo;} Q(s\u0026rsquo;, a\u0026rsquo;)\\) 作为 y 来训练神经网络,使用新的神经网络替换原有神经网络 重复收集数据和训练神经网络的步骤 在学习过程中选择合适的动作 上面提到我们要收集训练数据来训练我们的神经网络,如果我们全部随机选择下一步动作,训练的效果不会很好,而我们又不知道最合适的动作是什么。我们该如何选择下一步执行的动作呢?常见的做法是:\n大概率(比如:95%)的情况下,选择能够最大化 \\(Q(s\u0026rsquo;, a\u0026rsquo;)\\) 的动作 小概率(比如:5%)的情况下,选择一个随机的动作 这种方法叫做:\\(\\epsilon\\)-greedy policy 。\\(\\epsilon\\) 指的是选择随机动作的概率,在这里是 5%。还有一个常见的技巧是:开始时选择比较大的 \\(\\epsilon\\) ,随着训练的进行减小 \\(\\epsilon\\) 。\n优化 小批量学习(Mini-batch) 当我们 的训练数据量很大时,每一次我们的梯度下降都会求所有训练数据的导数,这会导致我们的训练进行的很慢。为了加快训练,我们可以每次训练不使用全部的数据,而是使用其中一部分数据。比如我们总共有一亿个训练数据,每次训练时,我们都使用不同的 1000 组数据来训练。这样可以加快我们的训练速度。当然,相比于使用全部数据来训练,这样会使我们的梯度下降变得不是很稳定,偶尔会朝着最小梯度以外的方向下降,但总体上还是朝着正确的方向下降。\n软更新(Soft Update) 之前提到的训练过程中会使用新训练好的神经网络来直接替换掉原来的神经网络,但有时我们新训练好的神经网络可能还不如原来的那个,这时替换的话会导致我们的拟合的更差。为了应对这种情况,我们可以采用软更新的方法:假设我们神经网络的参数是 W, B 那么,我们每次更新参数时不是直接使用 new_W , new_B 替换 W, B 。而是 W = 0.99W + 0.01new_W, B = 0.99B + 0.01new_B 。0.99 和 0.01 是可调的两个参数,要求相加等于 1 。这样可以使我们的神经网络拟合的更好。\n","permalink":"https://blog.raikiriww.net/post/416ace227c5e27b8/","summary":"\u003cp\u003e非监督学习\u003c/p\u003e","title":"机器学习(3)非监督学习"},{"content":"神经网络,决策树和优化\n神经网络(Neural network) 神经网络是模仿人类的神经元来设计的,虽然我们对人类大脑的具体原理还不清楚,但模仿神经元设计的神经网络的功能是非常强大的。\n结构 神经网络的结构是层级的,每一层(Layer)包含一个或多个神经元(Neuron)。每一个神经元都有自己的参数(\\(w\\),\\(b\\) ),从前一层获取值并经过自己的参数计算,传递给激活函数(Activation function),最终输出一个激活值。每层的所有神经元的激活值合并到一起就构成了这一层的输出。图例为两层的神经网络,第一层有三个神经元,第二层为输出层,有一个神经元。\n神经网络模型模型的输入会从左到右通过所有的层,得到一个输出,即神经网络的推断结果。这一过程也成为前向传播(Forward propagation)\n代码表示 使用目前比较流行的 Tensorflow 库来构建神经网络。代码示例:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import tensorflow as tf import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense from tensorflow.keras.activations import linear, relu, sigmoid X_train y_train # 构建模型 model = Sequential( [ Dense(3, activation = \u0026#39;relu\u0026#39;, name = \u0026#34;L1\u0026#34;), Dense(1, activation = \u0026#39;linear\u0026#39;, name = \u0026#34;L2\u0026#34;) ] ) # 指定损失函数和优化方式 model.compile( loss=tf.keras.losses.BinaryCrossentropy(from_logits=True), optimizer=tf.keras.optimizers.Adam(0.001), ) # 训练模型 model.fit( X_train,y_train, epochs=200 ) # 使用训练好的模型来预测 predictions = model.predict(X_testn) Sequential 用于指定神经网络模型的架构(有多少层,每层的类型),Dense 是一种层的类型,它可以指定该层的神经元个数,激活函数种类。compile 函数用于指定模型的代价函数和优化器类型。最后调用 fit 函数使用数据训练模型,epochs 用于指定训练数据训练多少次。Tensorflow 会在训练时将训练数据分为 batches 。每个 batch 大小为 32 。\n激活函数 目前有三种常用的激活函数:\nLinear:activation = \\( f(x) \\)。可以输出正负都很大的值。 sigmoid: activation = \\( \\frac{1}{1+e^{-f(x)}} \\)。输出的值的范围在 0 ~ 1 之间。 ReLU:activation = \\(max(0,f(x)) \\)。输出的值大于等于 0 。 一般建议在所有的非输出层,即隐藏层使用 ReLU 激活函数,它相比于 sigmoid 函数计算更快。如果在所有层使用 Linear 激活函数(或者不使用激活函数),则整个神经网络就退化为一个线性回归模型了。\nSoftmax softmax 是一种回归的类型,它可以从几个特定的种类中输出一个种类。类似逻辑回归,但逻辑回归只能输出 0 或 1, softmax 的结果可以是很多种类中的一个。借用吴恩达老师课程中的图片:\n用在神经网络中,最后的输出层输出的结果是一个向量,向量中的每个元素对应结果是该种类的概率值。该向量中值最大的元素的索引就是神经网络最终预测的种类。\n代价函数\n$$ \\mathbf{1}\\{y == n\\} = =\\begin{cases} 1, \u0026amp; \\text{if $y==n$}.\\\\ 0, \u0026amp; \\text{otherwise}. \\end{cases} $$\n$$ J(\\mathbf{w},b) = -\\frac{1}{m} \\left[ \\sum_{i=1}^{m} \\sum_{j=1}^{N} 1\\left\\{y^{(i)} == j\\right\\} \\log\\frac{e^{z^{(i)}_j}}{\\sum_{k=1}^N e^{z^{(i)}_k} } \\right] $$\n代码表示\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 model = Sequential( [ Dense(2, activation = \u0026#39;relu\u0026#39;, name = \u0026#34;L1\u0026#34;), Dense(4, activation = \u0026#39;linear\u0026#39;, name = \u0026#34;L2\u0026#34;) ] ) model.compile( loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), optimizer=tf.keras.optimizers.Adam(0.01) ) model.fit( X_train,y_train, epochs=200 ) 这样可以构建一个解决多个类别选一个问题的神经网络。最后一层使用 linear 激活函数而不是 softmax,在代价函数中加入 from_logits=True ,这样可以提高训练的精度。\n高级优化法 相比于梯度下降,一个更高级的优化方法是 Adam 法,相较于梯度下降,它的学习率 \\(\\alpha\\) 是不固定的,可以在训练过程中根据训练情况实时自动调整,避免学习率过大和过小导致的问题。使用方法很简单,上面的代码已经用到了 Adam 。只需在 compile 函数中指定 optimizer=tf.keras.optimizers.Adam(0.01) 即可。0.01为初始的学习率。\n其他类型的层 目前用到的层都是 Dense 层,它的每个神经元都会接受前一层的所有数据作为输入。还有一种叫做卷积(convolutional layer)的层,它的每个神经元接受前一层的特定位置的值作为输入。比如前一层输出了十个数据,这一层的第一个神经元只接收前三个,第二个神经元只接收第四到第六个作为输入。如果神经网络中由多个卷积层,这样的神经网络也叫卷积神经网络。组合多种不同的层可以构成不同的神经网络模型。\n反向传播 反向传播是神经网络训练过程的方式,由于神经网络由很多参数,很多层级,单纯的从输入开始计算偏导数到输出的计算量会很大。因为会涉及重复的运算。使用反向传播的方式,从输出开始向输入的方向计算每个参数的偏导数,使用链式法则将偏导数结合起来可以一次遍历计算完所有需要的导数。\n模型评估和优化 当模型在实际数据上的预测表现不佳时,有很多可以考虑做的优化方法,例如:收集更多的数据,尝试减少特征的数量,尝试寻找新的特征,尝试增加多项式特征,增大正则化系数,减小正则化系数等。这时候就需要正确评估什么方法是有效的。\n模型选择 选择使用什么样的模型来解决问题是很重要的一步,需要从多种模型(多项式,神经网络)中挑选最合适的那个。所以,我们将数据集分为三部分:\n训练集(training set),占总数据集的 60%,用于训练模型 交叉验证集(cross validation set),占总数据集的 20%,用于评估各个模型的效果 测试集(test set),占总数据集的 20%,用于测试交叉验证集选出的最优模型的泛化程度 流程为:先构建多个不同的模型,在训练集上将所有模型都训练好,然后计算所有模型在交叉验证集上的代价(cost),挑选出代价最小的那个模型,最后,计算挑选出的模型在测试集上的代价,用这个代价来评估模型的泛化程度。\nBias and Variance tradeoff 评估 Bias 和 Variance 需要一个基准,这个基准可以为:\n人类水平 竞争算法的水平 凭借经验的猜测 当模型的表现不佳时,一般分为两种情况,High bias(欠拟合) 或 High variance(过拟合)。两者都有的情况很少见。\nHigh bias 表现为在训练集上的准确率和基准相差很大,在交叉验证集上也和基准相差很大。 High variance 表现为在训练集上的准确率和基准近似,在交叉验证集上和基准相差很大。 根据这两种情况应该做不同的事情来优化模型:\nHigh bias:\n增加多项式特征 尝试增加特征的数量 降低正则化参数大小 High variance:\n增加正则化参数大小 尝试减少特征的数量 获取更多训练数据 神经网络被认为是 low bias model 。所以经常要处理的是 high variance 问题。\n错误分析(Error analysis) 错误分析(Error analysis)是指查看部分模型输出结果不对的数据,分析这些数据的类型。比如,有一个模型,它的输出结果是十个种类中的一个。查看100个模型输出错误的例子,其中,1个是类型1,20个是类型2,50个是类型3,等等。找到这之中占比比较大的结果来进行优化。在这个例子中就是类型3和类型2,根据这两个结果来优化模型会取得较大的效果。可以尝试多收集一些类型3和类型2的数据。或者调整模型参数(架构,正则化参数等等)。\n数据增强(Data argumentation) 数据增强是一种用来生成更多数据的方法。在输入的数据为图像时,可以对图像进行旋转,镜像,缩放,扭曲等操作来生成新的数据。如果输入是声音数据,可以对声音增加不同的背景噪音来生成新的数据。这些生成的新的数据可以让我们的模型训练的更好。\n转移学习(Transfer learning) 在神经网络中,一般会存在很多的层,当我们要训练的神经网络的输入和别人训练好的神经网络的输入相同时,比如:都是相同大小的图片。可以直接下载别人训练好的神经网络参数,把这些参数作为我们训练神经网络的初始参数。然后有两种做法:\n根据这些初始参数在我们的数据上重新训练所有的参数。 只根据我们的数据训练输出层的参数,其余参数不变。 别人训练好参数的步骤成为监督式预训练(supervised pretraining),我们根据别人训练好的参数来训练我们的模型的过程成为微调(fine tuning)。这种方式可以让我们不需要很多的数据就可以训练出我们自己的模型,还可以减少我们的训练时间。\n机器学习项目的完整循环 借用吴恩达老师课程的图片:\n避免歧视,不公,遵循伦理 当机器学习的模型涉及到对人进行评判和根据不同的人觉得不同的结果时,需要额外关注训练好的模型有没有对不同的人有歧视歧视,不公的现象,生成的内容有没有不遵循伦理的现象。目前并没有一些准则可以完全保证遵循了之后可以完全不会出现歧视。所以,在模型的构建过程中对模型进行不同这方面的测试是很有必要的。\n偏斜的数据集(Skewed datasets) 有时,我们的数据集在各种结果下的数据占比相差是非常大的。比如有一种很少见的疾病,发病率只有 0.5%。我们的数据集中有 0.5% 是患者。当我们根据这个数据集训练一个模型后,假设模型最后预测错了 1% 的数据,我们可能认为这个模型是一个比较不错的模型。但假设我们有另一个模型,不管数据的数据是什么,它都输出这个样本没有患病。这样,它预测错的比例只有 0.5%!比我们训练的模型还好。显然这个模型是来搞笑的,但它表现的就是很好。所以,我们需要新的方式来评估模型\n精确率/召回率(Precision/Recall) 实际(1) 实际(0) 预测(1) 真阳 假阳 预测(0) 假阴 真阴 精确率 = 真阳 / (真阳 + 假阳)\n召回率 = 真阳 / (真阳 + 假阴)\n当我们用这两个指标来评估模型时,我们上边一直预测为 0 的模型的这两个指标都会为 0 。我们就知道这个模型是不可用的。\n权衡 当我们采用了精确率/召回率来评判我们的模型后,我们可以根据具体的需求来调整判断阈值来让我们的预测更加符合要求。当我们想要增加模型的精确率时,即当我们的模型预测结果为 1 时,我们有很大的信心确定该样本就是阳性结果(比如当一种病的治疗会带来较大代价,而不治疗的代价相比来说更小)。我们可以调大阈值,比如从 0.5 调到 0.8。这样,我们模型的精确率就会增加。相反,当我们想要增加模型的召回率时,即我们想要尽可能的将可疑的样本都预测为阳性(比如当一种病放着不管的代价很大,早发现并治疗的代价很小)。我们可以调小阈值,比如从 0.5 调到 0.2 。这样,我们模型的召回率就会增加。\n可以看出,精确率和召回率是一个增大另一个就会减小,所以我们需要权衡它们。简单的将他们平均并不能帮助我们很好的权衡他们,所以我们采用 F1 score 来评估他们。\nF1 score 也叫 调和平均数(harmonic mean),具体公式为:\n$$ F1 score ={\\frac {2}{{\\frac {1}{x_{1}}}+{\\frac {1}{x_{2}}}}}={\\frac {2x_{1}x_{2}}{x_{1}+x_{2}}} $$\n调和平均数更关注数据中更小的项,当两个数据中一个很小时,它的调和平均数是比两个差不多的数据的调和平均数小的。所以,我们选择 F1 score 比较大的模型就可以得到更均衡的模型。\n决策树(Decision tree) 决策树是一种用来分类的模型,它可以从数据集中学习,根据数据集的特征来将数据集划分为几个子集。具体的过程为:\n在数据集上使用尝试所有特征来进行数据集的划分 计算所有划分后的信息增益 选取最大信息增益的特征作为划分的依据,在划分好的子集中重复以上步骤直至满足结束条件 信息增益(Information gain) 信息增益的计算公式为:\n$$ \\text{Information Gain} = H(p_1^\\text{node})- \\left(w^{\\text{left}}H\\left(p_1^\\text{left}\\right) + w^{\\text{right}}H\\left(p_1^\\text{right}\\right)\\right), $$\n\\(H\\) 是熵, 公式为:\n$$ H(p_1) = -p_1 \\text{log}_2(p_1) - (1- p_1) \\text{log}_2(1- p_1) $$\n\\(p\\) 是单个节点的纯度,是一个分数,分子为满足条件的数据数量,分母为该集合中所有数据的数量。\n结束条件 结束条件有很多选择:\n当一个节点全部都属于一个种类 当树的深度达到设定的最大值 继续划分的信息增益小于设定的阈值 one-hot encoding 有时我们数据的特征不只是有或没有,而是好几个选项中的一个。这时,我们需要将这个特征进行 one hot encoding。\n例: 有一个特征为皮肤颜色,它可以是黑色,白色,黄色。我们需要将这个特征分为三个特征:是否黑色皮肤,是否白色皮肤,是否黄色皮肤。这样就可以满足决策树的特征划分要求。\n连续值特征处理 当一个特征为连续的值时,比如体重。我们需要将这个特征分别按照几个阈值进行划分,计算每次划分前和后熵的差值,选取差值最大的那个作为阈值。当该特征的值小于这个阈值时将这个值转换为 0 ,大于则转换为 1 。\n阈值可以按照划分数据集的权重来从头尝试到尾。比如有十个数据,将它从小到大排序,一开始将最小的那个划分到左边,剩下九个划分到右边,计算熵的差值。然后将最小的两个划分到左边,剩下八个划分到右边。。。。。。\n回归树 相较于分类的决策树,回归树的输出值不是特定的。它可以输出连续的值。它在划分子集的时候用到的依据不是熵减的大小,而是方差的大小。没每次选取方差减小的最大的那个特征作为划分依据。最后输出的结果是该子集中所有数据的平均值。\nTree Ensembles 单个决策树对数据中的微小变化很敏感,为了提高模型的鲁棒性我们可以训练很多个决策树,我们可以称这些决策树为一个 tree ensemble。我们最终的结果是这个 tree ensemble 中的所有树的输出中最多的那个。为了构建这样的树,我们需要用到有放回的抽样(Sampling with replacement)。假设总共有 10 个数据,我们可以在这个 10 个数据上使用有放回抽样 3 次,这样我们就会得到三组额外的数据,可以在这三组数据上训练出三个不同的决策树。\nRandom forest algorithm 随机森林算法(Random forest algorithm)会使用上边说到的有放回抽样,重复 B 次。假设我们有 n 个特征,当每个节点要选取特征作为划分的依据时,它不会从所有的特征中选择,而是从一个具有 k (k \u0026lt; n) 个特征的随机子集中选择信息增益最大的那个特征。k 一般取 n 的平方根。\nXGBOOST(eXtreme Gradient Boosting) XGBOOST 是一种更强的决策树模型,相较于普通的决策树,它每次有放回抽样时会优先考虑之前的决策树分类错误的样本,也就是刻意练习。这会让我们的模型训练的效果更好。它也包含了正则化来防止过拟合。代码示例为:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # Classification from xgboost import XGBClassifier model = XGBClassifier() model.fit(x_train, y_train) y_pred = model.predict(X_test) # Regression from xgboost import XGBRegressor model = XGBRegressor() model.fit(x_train, y_train) y_pred = model.predict(X_test) 神经网络和决策树的抉择 决策树 当我们的数据时结构化的数据,比如数字,种类等。我们可以考虑使用决策树。相较于神经网络模型,决策树的训练会更快速,较小的决策树叶更容易人类理解。\n神经网络 当我们的数据是非结构化的数据,比如图片,语言等,我们需要使用神经网络来构建模型,当然,神经网络模型也可以根据结构化的数据构建,它在这两种数据上的效果都很好。但神经网络相较于决策树会训练的慢一些,导致我们迭代模型的速度也会较慢。不过神经网络可以借助 transfer learning 来更快更好的训练。当我们需要构建一个多模型共同运行的系统时,神经网络模型相较于决策树更容易整合。\n","permalink":"https://blog.raikiriww.net/post/4f14d32a25efe612/","summary":"\u003cp\u003e神经网络,决策树和优化\u003c/p\u003e","title":"机器学习(2) 神经网络, 决策树和优化"},{"content":"机器学习基础知识\n机器学习种类 机器学习种类大概可以分为监督式学习(Supervised learning)和非监督式学习(Unsupervised learning)。\n监督式学习(Supervised learning) 监督式学习算法会从给定的数据中学习,数据中包含输入和对应的正确输出。最终,监督式学习算法可以将输入与输出建立对应关系,从而实现给它一个输入,它能根据输入输出一个结果。\n监督式学习有很多算法,目前常用的有下面几种。\n算法 回归(Regression) 分类(Classification) 结果类型 预测数字 预测种类 结果数量 可能的结果是无限的 可能的结果是有限的 例子 根据房子面积,楼层预测房价 根据输入的图片预测是猫还是狗 非监督式学习(Unsupervised learning) 非监督式学习训练的数据中不包含对应的输出。所以它用来寻找数据中的有趣的模式或者关联,而不是输出特定的结果。常见的非监督式学习算法有:聚合(Clustering),异常检测(Anormaly detection),降维(Dimensionally reduction)。\n线性回归(Linner regression) 公式 $$ f_{w,b}(x^{(i)}) = wx^{(i)} + b \\tag{1}\\nonumber $$\n\\( w \\), \\( b \\) 是参数\n代价函数 代价函数的作用是评估我们的模型和训练数据的拟合程度,越小代表我们的模型拟合的越好。常用的有平方代价函数,数学公式为:\n$$ J(w,b) = \\frac{1}{2m} \\sum\\limits_{i = 0}^{m-1} (f_{w,b}(x^{(i)}) - y^{(i)})^2 $$\n目标 $$ \\underset{w, b}{\\text{minimize}}J(w,b)$$\n梯度下降(Gradient descent) 梯度下降是一种常用的模型训练方法,它可以逐步更新参数直至找到代价函数的局部最小值。当代价函数使用平方时,代价函数的形状会是碗形(Convex shape)。这意味着局部最小值也是全局最小值,所以可以用梯度下降来训练模型参数直至参数到达全局最小值。梯度下降的数学公式为:\n$$ w = w - \\alpha \\frac{\\partial J(w,b)}{\\partial w} $$ $$ b = b - \\alpha \\frac{\\partial J(w,b)}{\\partial b} $$\n$$ \\frac{\\partial J(w,b)}{\\partial w} = \\frac{1}{m} \\sum\\limits_{i = 0}^{m-1} (f_{w,b}(x^{(i)}) - y^{(i)})x^{(i)} $$\n$$ \\frac{\\partial J(w,b)}{\\partial b} = \\frac{1}{m} \\sum\\limits_{i = 0}^{m-1} (f_{w,b}(x^{(i)}) - y^{(i)}) $$\n\\(w\\) 和 \\(b\\) 需要同步更新。\n\\(\\alpha\\) 为学习率,不能太大也不能太小。\\(\\alpha\\) 太小会导致每次训练参数变化值的很小,可能训练很久也没多大变化。\\(\\alpha\\) 太大会导致拟合不了,代价会越来越大,导致离散(diverge)。\n学习率在梯度下降的过程中是不变的,\\(w\\) 和 \\(b\\) 的偏导数会随着训练变小。所以随着训练次数的增加,代价的减小幅度是在变小的。\n运行 这样就集齐了所有的要素,现在可以通过对所有数据重复运行梯度下降来训练模型参数。\n向量化(Vectorization) 当数据量很大的时候,使用循环来一个一个计算是很慢的。现代的 CPU 和 GPU 都实现了 Single Instruction, Multiple Data (SIMD) pipelines 功能,这允许并行执行多个指令,在数据量大的时候可以极大的加快我们的计算速度。在 Python 中,可以使用 numpy 库来创建向量。\n多元线性回归(Multiple linear regression) 公式 $$ f_{\\mathbf{w},b}(\\mathbf{x}) = \\mathbf{w} \\cdot \\mathbf{x} + b $$\n$$ \\mathbf{w} = \\begin{pmatrix} w_0 \\\\ w_1 \\\\ \\cdots\\\\ w_{n-1} \\end{pmatrix}$$\n\\( b \\) 是一个数字\n代价函数 $$J(\\mathbf{w},b) = \\frac{1}{2m} \\sum\\limits_{i = 0}^{m-1} (f_{\\mathbf{w},b}(\\mathbf{x}^{(i)}) - y^{(i)})^2 $$\n\\( \\mathbf{w} \\) 和 \\( \\mathbf{x}^{(i)} \\) 都是向量\n梯度下降 $$\\begin{align*} \\text{repeat}\u0026amp;\\text{ until convergence:} \\lbrace \\newline; \u0026amp; w_j = w_j - \\alpha \\frac{\\partial J(\\mathbf{w},b)}{\\partial w_j} \u0026amp; \\text{for j = 0..n-1}\\ \\newline \u0026amp;b\\ \\ = b - \\alpha \\frac{\\partial J(\\mathbf{w},b)}{\\partial b} \\newline \\rbrace \\end{align*}$$ $$ \\begin{align} \\frac{\\partial J(\\mathbf{w},b)}{\\partial w_j} \u0026amp;= \\frac{1}{m} \\sum\\limits_{i = 0}^{m-1} (f_{\\mathbf{w},b}(\\mathbf{x}^{(i)}) - y^{(i)})x_{j}^{(i)} \\\\ \\frac{\\partial J(\\mathbf{w},b)}{\\partial b} \u0026amp;= \\frac{1}{m} \\sum\\limits_{i = 0}^{m-1} (f_{\\mathbf{w},b}(\\mathbf{x}^{(i)}) - y^{(i)}) \\end{align} $$ m 是训练集的样本数,n 是特征数\n特征缩放(Feature scaling) 当有多个 x 且 x 的数量级差距较大时(一两个数量级以上),由于学习率 \\( \\alpha \\) 对于每个特征都是固定的,这会导致数量级大的特征拟合的很快,然后拖慢其他所有的特征的拟合速度,使得整体的拟合速度很慢。所以,需要将所有特征缩放到差不多的数量级再进行训练,常用的方法有:\n当特征中的每个值都为正时,将每个特征都除以它的最大值,可以保证缩放后的特征都在 0 ~ 1 之间。 Mean normalization: \\( x_i := \\dfrac{x_i - \\mu_i}{max - min} \\)。将每个特征都减去平均值然后除以最大值和最小值的差值。可以适用于任何场景。缩放后的值在 -1 ~ 1 中。 Z-score normalization: \\(x^{(i)}_j = \\dfrac{x^{(i)}_j - \\mu_j}{\\sigma_j} \\) 将每个特征都将去平均值然后除以标准差。可以适用于任何场景。缩放后的值均值为 0 ,标准差为 1。 学习率(Learning rate) 学习率需要经过尝试来选择一个最好的值。可以通过迭代固定的较小的次数,观察代价随着迭代次数的变化曲线。如果代价随着迭代升高,证明学习率过大或代码中有 Bug 。如果下降很缓慢,可能是学习率过小,可以适当调高学习率(每次大3倍)直至下降的很快或着代价开始随着迭代上升。\n特征工程(Feature engineering) 特征工程是指通过观察和直觉选择合适的特征参与训练。比如一个房子的长和宽是两个特征,可以将它们两个相乘来得到一个新特征:面积。这个特征会比长和宽更好。\n在一些比较复杂的模型中,比如多项式回归(Polynomial regression),选择合适的特征是很重要的。可以在一开始多加几个特征,进行一定次数的迭代。假如一开始的特征为 \\( y=w_0x_0 + w_1x_1^2 +b\\) 。可以将其扩展为: \\( y=w_0x_0 + w_1x_1^2 + w_2x_2^3+b \\)。迭代几千次 。重要的特征参数会在迭代中变大,不重要的特征会随着迭代趋近于 0。可以根据迭代后的特征对应的参数大小来判断合适的特征。\n逻辑回归(Logistic regression) 分类任务需要输出特定的值,比如 1, 0。而线性回归的结果是无穷多的。所以我们需要一个新的模型来处理这种情况,也就是逻辑回归。\nsigmod 函数 sigmod 函数是逻辑回归中的重要函数,他们可以将结果限制在 0 ~1 的区间内,方便我们将结果归到 1 或 0 。具体公式为:\n$$ g(x) = \\frac{1}{1+e^{x}} $$\n决策边界(Decision boundry) 有了 sigmod 函数后,我们把结果限制在了 0~1 的区间内。但我们想要的是 0 或 1,所以,我们需要一个边界,高于这个边界的我们认为结果是 1。低于这个边界的我们认为结果是 0。这个边界就是决策边界。边界可以根据不同需求来调整,一般 0.5 可以认为是一个合理的决策边界。\n公式 将线性回归公式的 \\( f_{\\mathbf{w},b}(\\mathbf{x}) \\) 作为 \\( x \\) 带入 sigmod 函数中即可得到逻辑回归的公式: $$ \\begin{align} f_{\\mathbf{w},b}(\\mathbf{x^{(i)}}) = g(z^{(i)}) \\\\ g(z^{(i)}) = \\frac{1}{1+e^{-z^{(i)}}} \\\\ z^{(i)} = \\mathbf{w} \\cdot \\mathbf{x}^{(i)}+ b \\\\ \\end{align} $$\n代价函数 逻辑回归不能用和线性回归一样的平方代价函数。因为它的代价函数的形状不是平滑的,存在很多个局部最小值,这会导致使用平台代价函数梯度下降时卡在某个局部最小值而不是下降到全局最小值。所以,我们需要一个新的代价函数:\n$$ J(\\mathbf{w},b) = \\frac{1}{m} \\sum_{i=0}^{m-1} \\left[ loss(f_{\\mathbf{w},b}(\\mathbf{x}^{(i)}), y^{(i)}) \\right] $$\n\\( loss(f_{\\mathbf{w},b}(\\mathbf{x}^{(i)}), y^{(i)}) \\) 指的是每个样本的损失函数:\n$$loss(f_{\\mathbf{w},b}(\\mathbf{x}^{(i)}), y^{(i)}) = -y^{(i)} \\log\\left(f_{\\mathbf{w},b}\\left( \\mathbf{x}^{(i)} \\right) \\right) - \\left( 1 - y^{(i)}\\right) \\log \\left( 1 - f_{\\mathbf{w},b}\\left( \\mathbf{x}^{(i)} \\right) \\right) $$\n看着很复杂,但考虑到 \\( y^{(i)} \\) 只有 1 或 0 两个可能的值,损失函数就可以简化为两种情况,就不复杂了。使用这个新的代价函数,我们可以在逻辑回归的情况下得到一个平滑的,局部最小值就是全局最小值的凸形曲线。这样就可以使用梯度下降来训练参数了。\n梯度下降 $$\\begin{align*} \u0026amp;\\text{repeat until convergence:} \\; \\lbrace \\\\ \u0026amp; \\; \\; \\; w_j = w_j - \\alpha \\frac{\\partial J(\\mathbf{w},b)}{\\partial w_j} \\; \u0026amp; \\text{for j := 0..n-1} \\\\ \u0026amp; \\; \\; \\; \\; \\;b = b - \\alpha \\frac{\\partial J(\\mathbf{w},b)}{\\partial b} \\\\ \u0026amp;\\rbrace \\end{align*}$$\n$$\\begin{align*} \\frac{\\partial J(\\mathbf{w},b)}{\\partial w_j} \u0026amp;= \\frac{1}{m} \\sum\\limits_{i = 0}^{m-1} (f_{\\mathbf{w},b}(\\mathbf{x}^{(i)}) - y^{(i)})x_{j}^{(i)} \\\\ \\frac{\\partial J(\\mathbf{w},b)}{\\partial b} \u0026amp;= \\frac{1}{m} \\sum\\limits_{i = 0}^{m-1} (f_{\\mathbf{w},b}(\\mathbf{x}^{(i)}) - y^{(i)}) \\end{align*}$$\n过拟合(Overfitting) 当模型在训练数据上拟合的太过了,模型会在训练数据上表现的很好,它的输出可能与 y 完全相同。但当遇到未被训练过的数据,即新数据时,模型的表现会很差。这中现象就是过拟合,它的泛化(generalization)程度不够,不能实际使用。这种情况也叫 High Variance。有几种方法可以解决这个问题:\n获取更多数据 这是最简单直观的方法,更多的数据会让模型更不容易训练的效果更好,更不容易过拟合。\n选择合适的特征 有些特征可能是非必要的,当拥有很多特征时,可以考虑去掉一些,保留关键特征。\n正则化(Regularization) 正则化是指在代价函数中增加一个惩罚项,这样可以在训练的过程中减小 \\( w \\) (也可以选择加上正则化 \\( b \\) ),鼓励模型学习更简单或平滑的函数,提高模型的泛化能力。\n正则化线性回归 $$J(\\mathbf{w},b) = \\frac{1}{2m} \\sum\\limits_{i = 0}^{m-1} (f_{\\mathbf{w},b}(\\mathbf{x}^{(i)}) - y^{(i)})^2 + \\frac{\\lambda}{2m} \\sum\\limits_{j=0}^{n-1} w_j^2 $$\n$$ f_{\\mathbf{w},b}(\\mathbf{x}^{(i)}) = \\mathbf{w} \\cdot \\mathbf{x}^{(i)} + b $$\n相比于为正则化的代价函数多出的: \\( \\frac{\\lambda}{2m} \\sum\\limits_{j=0}^{n-1} w_j^2 \\) 即是惩罚项。\n正则化逻辑回归 $$J(\\mathbf{w},b) = \\frac{1}{m} \\sum\\limits_{i=0}^{m-1} \\left[ -y^{(i)} \\log\\left(f_{\\mathbf{w},b}\\left( \\mathbf{x}^{(i)} \\right) \\right) - \\left( 1 - y^{(i)}\\right) \\log \\left( 1 - f_{\\mathbf{w},b}\\left( \\mathbf{x}^{(i)} \\right) \\right) \\right] + \\frac{\\lambda}{2m} \\sum\\limits_{j=0}^{n-1} w_j^2 $$\n$$ f_{\\mathbf{w},b}(\\mathbf{x}^{(i)}) = sigmoid(\\mathbf{w} \\cdot \\mathbf{x}^{(i)} + b) $$\n相比于为正则化的代价函数多出的: \\( \\frac{\\lambda}{2m} \\sum\\limits_{j=0}^{n-1} w_j^2 \\) 即是惩罚项。\n梯度下降 对于线性回归和逻辑回归,正则化后的梯度下降只有 \\( f_{\\mathbf{w},b}(\\mathbf{x}^{(i)}) \\) 的定义不同\n$$\\begin{align*} \\frac{\\partial J(\\mathbf{w},b)}{\\partial w_j} \u0026amp;= \\frac{1}{m} \\sum\\limits_{i = 0}^{m-1} (f_{\\mathbf{w},b}(\\mathbf{x}^{(i)}) - y^{(i)})x_{j}^{(i)} + \\frac{\\lambda}{m} w_j \\\\ \\frac{\\partial J(\\mathbf{w},b)}{\\partial b} \u0026amp;= \\frac{1}{m} \\sum\\limits_{i = 0}^{m-1} (f_{\\mathbf{w},b}(\\mathbf{x}^{(i)}) - y^{(i)}) \\end{align*}$$\n因为每次更新 \\(w_j\\) 的时候都增加了一个正数的惩罚项,所以每次更新都会导致 \\(w_j\\) 变小一点,这就是正则化的作用。\n","permalink":"https://blog.raikiriww.net/post/0a586aa7b0e76b03/","summary":"\u003cp\u003e机器学习基础知识\u003c/p\u003e","title":"机器学习(1)-线性回归和逻辑回归"},{"content":"一直以来项目都是路由到特定端口的根目录,简单配置一下就好。今天要部署一套前端到指定的子目录,复制了一份之前的根目录 server 配置,简单的将 location 的路由地址由 /替换为指定子目录后,其他配置不变。部署上去测试发现直接 404 Not Found。😐\n问题分析 大概的 Nginx 配置如下:\n1 2 3 4 5 6 7 8 9 10 server { listen 8000; server_name www.test.cn; location /20002 { root /www/test/20002/dist; index index.html; } } 这个配置是直接从其他根目录的 server 复制过来的。打开 server_name:8000/20002 会直接报 404 Not Found。\n首先去绑定的目录 /www/test/20002/dist 下查看是否由对应 Index.html 文件,有的,不是网页文件的问题。那应该就是我配置文件写的不对。将 location 改为 /,重启 Nginx,直接访问 server_name:8000 可以成功显示页面。问题应该是处在子目录的路由配置上。配置里就两个参数。。\nroot 用来指定路由到哪里。 index 用来设置默认访问的文件。 看起来都没啥问题,尝试进行搜索。\n寻找原因 搜索 Nginx 路由子目录的方法,查询到 Nginx 路由静态网页由两种方法,一个是 root,一个是 alias。两种方法对请求文件的寻找路径不同。\nroot\n当使用 root 指令并且没有使用精确匹配(使用 =)时,root 方法会将请求的路径拼接到 root 所设置的路径后。最终路径为:root + location alias\n当使用 alias 指令时,alias 方法会将访问的文件路径用 alias 替换。最终路径为:alias 举个例子:\n1 2 3 4 location /20002 { root /www/test/20002/dist; # ... } 使用 root 方法来路由时,当我们访问 /20002/css/style.css 时,Nginx 会寻找 /www/test/20002/dist/20002/css/style.css 文件。\n1 2 3 4 location /20002 { alias /www/test/20002/dist; # ... } 使用 alias 来路由时,当我们访问 /20002/css/style.css 时,Nginx 会寻找 /www/test/20002/dist/css/style.css 文件。\n所以,当我直接复制根目录的配置,简单修改一下 location 来进行子页面的路由时会出现 404 Not Found 的错误。当我访问 server_name:8000/20002/index.html 时,实际上 Nginx 在寻找 /www/test/20002/dist/20002/index.html 。是找不到这个文件的。\n解决方法 找到了报错原因那解决就很简单了,根据上面的说明将 root 方法更改为 alias 即可。最终代码为:\n1 2 3 4 5 6 7 8 9 10 server { listen 8000; server_name www.test.cn; location /20002 { alias /www/test/20002/dist; index index.html; } } 这样访问 server_name:8000/20002/index.html 会被 Nginx 路由到 /www/test/20002/dist/index.html 这样路径就是正确的了。\n参考 https://stackoverflow.com/questions/10631933/nginx-static-file-serving-confusion-with-root-alias https://juejin.cn/post/6919799574253174792 ","permalink":"https://blog.raikiriww.net/post/d43d83a85cc2d89c/","summary":"\u003cp\u003e一直以来项目都是路由到特定端口的根目录,简单配置一下就好。今天要部署一套前端到指定的子目录,复制了一份之前的根目录 server 配置,简单的将 location 的路由地址由 \u003ccode\u003e/\u003c/code\u003e替换为指定子目录后,其他配置不变。部署上去测试发现直接 404 Not Found。😐\u003c/p\u003e","title":"Nginx 配置子目录路由"},{"content":"","permalink":"https://blog.raikiriww.net/test/","summary":"","title":"test"},{"content":"利用 Github Actions 来自动构建和部署博客到 Github Pages。这样既可以简化自己的操作,又能保证自己的博客源码的私密性。\nGithub token 具体位置为:个人设置界面 -\u0026gt; Developer Settings -\u0026gt; Personal access tokens -\u0026gt; Tokens(classic) 在此界面新生成一个 token,需要勾选 repo 和 workflow 选项。Expiration 可以设置为 No expiration。创建完成后会显示你的 token,它只会显示这一次,你需要将它记下来,后边会用到。。\nworkflow 文件 想利用 Github Actions,需要在博客的根目录下创建 .github/workflows/ 文件夹。在该文件夹下创建 yml 文件会被 Github Actions 执行。\n我的 workflow 文件如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 name: GitHub Pages on: push: branches: - main # Set a branch to deploy release: types: - published jobs: deploy: runs-on: ubuntu-20.04 concurrency: group: ${{ github.workflow }}-${{ github.ref }} steps: - uses: actions/checkout@v3 with: ref: main - name: Setup Hugo uses: peaceiris/actions-hugo@v2 with: hugo-version: \u0026#39;0.119.0\u0026#39; # 是否启用 hugo extend # extended: true - name: Build run: hugo --minify - name: Deploy run: | cd ./public git init git config --global user.name \u0026#39;${{ secrets.GITHUBUSERNAME }}\u0026#39; git config --global user.email \u0026#39;${{ secrets.GITHUBEMAIL }}\u0026#39; git add . git commit -m \u0026#34;${{ github.event.head_commit.message }}\u0026#34; git push --force --quiet \u0026#34;https://${{ secrets.GITHUBUSERNAME }}:${{ secrets.GITHUBTOKEN }}@github.com/${{ secrets.GITHUBUSERNAME }}/${{ secrets.GITHUBUSERNAME }}.github.io.git\u0026#34; master:main #git push --force --quiet \u0026#34;https://${{ secrets.TOKENUSER }}:${{ secrets.CODINGTOKEN }}@e.coding.net/${{ secrets.CODINGUSERNAME }}/${{ secrets.CODINGBLOGREPO }}.git\u0026#34; master:master #coding 部署写法,需要的自行取消注释 #git push --force --quiet \u0026#34;https://${{ secrets.GITEEUSERNAME }}:${{ secrets.GITEETOKEN }}@gitee.com/${{ secrets.GITEEUSERNAME }}/${{ secrets.GITEEUSERNAME }}.git\u0026#34; master:master #gitee 部署写法,需要的自行取消注释 其中,GITHUBUSERNAME ,GITHUBEMAIL , GITHUBTOKEN 三个为自定义变量,后边会讲到。\n源码仓库 新建或使用一个老的仓库,可见性设置为 private。在代码仓库的 Settings 页面找到 Secrets and variables ,点击其中的 Actions ,添加 workflow 中用到的三个变量:GITHUBUSERNAME ,GITHUBEMAIL , GITHUBTOKEN 。\n上传代码 完成上述步骤后即可用 git 提交代码到你的源码仓库,即可在仓库的 Actions 界面看到执行的 workflows。\n参考 整体结构 部署部分 ","permalink":"https://blog.raikiriww.net/post/1ed1b0494a419b40/","summary":"\u003cp\u003e利用 Github Actions 来自动构建和部署博客到 Github Pages。这样既可以简化自己的操作,又能保证自己的博客源码的私密性。\u003c/p\u003e","title":"使用 Github Actions 自动部署博客"},{"content":"此篇博客介绍我的博客的修改内容和方法。我的主题为 hugo-PaperMod。我修改前都将博客内的资源文件夹 (assets, i18n, lay) 复制到了项目根目录中,这样以后更新主题也不会导致修改消失。。\n石蒜小组件 来自与 Github。项目地址为:https://github.com/dsrkafuu/sakana-widget。具体更改为在 /layouts/partials/extend_footer.html 中加入如下代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 \u0026lt;!-- 石蒜组件 --\u0026gt; \u0026lt;style\u0026gt; html #sakana-widget{ position: fixed; right: 50px; bottom: 0; transform-origin: 100% 100%; /* 从右下开始变换 */ } \u0026lt;/style\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;https://cdn.jsdelivr.net/npm/[email protected]/lib/sakana.min.css\u0026#34; /\u0026gt; \u0026lt;div id=\u0026#34;sakana-widget\u0026#34;, style=\u0026#34;z-index:999\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; function initSakanaWidget() { new SakanaWidget().mount(\u0026#39;#sakana-widget\u0026#39;); } \u0026lt;/script\u0026gt; \u0026lt;script async onload=\u0026#34;initSakanaWidget()\u0026#34; src=\u0026#34;https://cdn.jsdelivr.net/npm/[email protected]/lib/sakana.min.js\u0026#34; \u0026gt;\u0026lt;/script\u0026gt; 重新编译后即可拥有可爱石蒜小组件。 High 一下 有两种 High 一下的代码实现:\n第一种:来自于 左耳朵耗子 的网站,代码为: 1 javascript:(function(){function c(){var e=document.createElement(\u0026#34;link\u0026#34;);e.setAttribute(\u0026#34;type\u0026#34;,\u0026#34;text/css\u0026#34;);e.setAttribute(\u0026#34;rel\u0026#34;,\u0026#34;stylesheet\u0026#34;);e.setAttribute(\u0026#34;href\u0026#34;,f);e.setAttribute(\u0026#34;class\u0026#34;,l);document.body.appendChild(e)}function h(){var e=document.getElementsByClassName(l);for(var t=0;t\u0026lt;e.length;t++){document.body.removeChild(e[t])}}function p(){var e=document.createElement(\u0026#34;div\u0026#34;);e.setAttribute(\u0026#34;class\u0026#34;,a);document.body.appendChild(e);setTimeout(function(){document.body.removeChild(e)},100)}function d(e){return{height:e.offsetHeight,width:e.offsetWidth}}function v(i){var s=d(i);return s.height\u0026gt;e\u0026amp;\u0026amp;s.height\u0026lt;n\u0026amp;\u0026amp;s.width\u0026gt;t\u0026amp;\u0026amp;s.width\u0026lt;r}function m(e){var t=e;var n=0;while(!!t){n+=t.offsetTop;t=t.offsetParent}return n}function g(){var e=document.documentElement;if(!!window.innerWidth){return window.innerHeight}else if(e\u0026amp;\u0026amp;!isNaN(e.clientHeight)){return e.clientHeight}return 0}function y(){if(window.pageYOffset){return window.pageYOffset}return Math.max(document.documentElement.scrollTop,document.body.scrollTop)}function E(e){var t=m(e);return t\u0026gt;=w\u0026amp;\u0026amp;t\u0026lt;=b+w}function S(){var e=document.createElement(\u0026#34;audio\u0026#34;);e.setAttribute(\u0026#34;class\u0026#34;,l);e.src=i;e.loop=false;e.addEventListener(\u0026#34;canplay\u0026#34;,function(){setTimeout(function(){x(k)},500);setTimeout(function(){N();p();for(var e=0;e\u0026lt;O.length;e++){T(O[e])}},15500)},true);e.addEventListener(\u0026#34;ended\u0026#34;,function(){N();h()},true);e.innerHTML=\u0026#34; \u0026lt;p\u0026gt;If you are reading this, it is because your browser does not support the audio element. We recommend that you get a new browser.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;\u0026#34;;document.body.appendChild(e);e.play()}function x(e){e.className+=\u0026#34; \u0026#34;+s+\u0026#34; \u0026#34;+o}function T(e){e.className+=\u0026#34; \u0026#34;+s+\u0026#34; \u0026#34;+u[Math.floor(Math.random()*u.length)]}function N(){var e=document.getElementsByClassName(s);var t=new RegExp(\u0026#34;\\\\b\u0026#34;+s+\u0026#34;\\\\b\u0026#34;);for(var n=0;n\u0026lt;e.length;){e[n].className=e[n].className.replace(t,\u0026#34;\u0026#34;)}}var e=30;var t=30;var n=350;var r=350;var i=\u0026#34;//s3.amazonaws.com/moovweb-marketing/playground/harlem-shake.mp3\u0026#34;;var s=\u0026#34;mw-harlem_shake_me\u0026#34;;var o=\u0026#34;im_first\u0026#34;;var u=[\u0026#34;im_drunk\u0026#34;,\u0026#34;im_baked\u0026#34;,\u0026#34;im_trippin\u0026#34;,\u0026#34;im_blown\u0026#34;];var a=\u0026#34;mw-strobe_light\u0026#34;;var f=\u0026#34;//s3.amazonaws.com/moovweb-marketing/playground/harlem-shake-style.css\u0026#34;;var l=\u0026#34;mw_added_css\u0026#34;;var b=g();var w=y();var C=document.getElementsByTagName(\u0026#34;*\u0026#34;);var k=null;for(var L=0;L\u0026lt;C.length;L++){var A=C[L];if(v(A)){if(E(A)){k=A;break}}}if(A===null){console.warn(\u0026#34;Could not find a node of the right size. Please try a different page.\u0026#34;);return}c();S();var O=[];for(var L=0;L\u0026lt;C.length;L++){var A=C[L];if(v(A)){O.push(A)}}})() 第二种:来自于 Github 的 代码 : 1 javascript:(function(){function h(){var e=document.createElement(\u0026#34;link\u0026#34;);e.setAttribute(\u0026#34;type\u0026#34;,\u0026#34;text/css\u0026#34;);e.setAttribute(\u0026#34;rel\u0026#34;,\u0026#34;stylesheet\u0026#34;);e.setAttribute(\u0026#34;href\u0026#34;,l);e.setAttribute(\u0026#34;class\u0026#34;,c);document.body.appendChild(e)}function p(){var e=document.getElementsByClassName(c);for(var t=0;t\u0026lt;e.length;t++){document.body.removeChild(e[t])}}function d(){var e=document.createElement(\u0026#34;div\u0026#34;);e.setAttribute(\u0026#34;class\u0026#34;,f);document.body.appendChild(e);setTimeout(function(){document.body.removeChild(e)},100)}function v(e){return{height:e.offsetHeight,width:e.offsetWidth}}function m(i){var s=v(i);return s.height\u0026gt;e\u0026amp;\u0026amp;s.height\u0026lt;n\u0026amp;\u0026amp;s.width\u0026gt;t\u0026amp;\u0026amp;s.width\u0026lt;r}function g(e){var t=e;var n=0;while(!!t){n+=t.offsetTop;t=t.offsetParent}return n}function y(){var e=document.documentElement;if(!!window.innerWidth){return window.innerHeight}else if(e\u0026amp;\u0026amp;!isNaN(e.clientHeight)){return e.clientHeight}return 0}function b(){if(window.pageYOffset){return window.pageYOffset}return Math.max(document.documentElement.scrollTop,document.body.scrollTop)}function S(e){var t=g(e);return t\u0026gt;=E\u0026amp;\u0026amp;t\u0026lt;=w+E}function x(){var e=document.createElement(\u0026#34;audio\u0026#34;);e.setAttribute(\u0026#34;class\u0026#34;,c);e.src=i;e.loop=false;var t=false,n=false,r=false;e.addEventListener(\u0026#34;timeupdate\u0026#34;,function(){var i=e.currentTime,s=D,o=s.length,u;if(i\u0026gt;=.5\u0026amp;\u0026amp;!t){t=true;T(_)}if(i\u0026gt;=15.5\u0026amp;\u0026amp;!n){n=true;k();d();for(u=0;u\u0026lt;o;u++){N(s[u])}}if(e.currentTime\u0026gt;=28.4\u0026amp;\u0026amp;!r){r=true;C()}},true);e.addEventListener(\u0026#34;ended\u0026#34;,function(){k();p()},true);e.innerHTML=\u0026#34;\u0026lt;p\u0026gt;If you are reading this, it is because your browser does not support the audio element. We recommend that you get a new browser.\u0026lt;/p\u0026gt;\u0026#34;;document.body.appendChild(e);e.play()}function T(e){e.className+=\u0026#34; \u0026#34;+s+\u0026#34; \u0026#34;+u}function N(e){e.className+=\u0026#34; \u0026#34;+s+\u0026#34; \u0026#34;+a[Math.floor(Math.random()*a.length)]}function C(){var e=document.getElementsByClassName(s);for(var t=0;t\u0026lt;e.length;){e[t].className=e[t].className.replace(s,o)}s=o}function k(){var e=document.getElementsByClassName(s);var t=new RegExp(\u0026#34;\\\\b\u0026#34;+s+\u0026#34;\\\\b\u0026#34;);for(var n=0;n\u0026lt;e.length;){e[n].className=e[n].className.replace(t,\u0026#34;\u0026#34;)}}var e=30;var t=30;var n=350;var r=350;var i=\u0026#34;//s3.amazonaws.com/moovweb-marketing/playground/harlem-shake.ogg\u0026#34;;var s=\u0026#34;mw-harlem_shake_me\u0026#34;;var o=\u0026#34;mw-harlem_shake_slow\u0026#34;;var u=\u0026#34;im_first\u0026#34;;var a=[\u0026#34;im_drunk\u0026#34;,\u0026#34;im_baked\u0026#34;,\u0026#34;im_trippin\u0026#34;,\u0026#34;im_blown\u0026#34;];var f=\u0026#34;mw-strobe_light\u0026#34;;var l=\u0026#34;//s3.amazonaws.com/moovweb-marketing/playground/harlem-shake-style.css\u0026#34;;var c=\u0026#34;mw_added_css\u0026#34;;var w=y();var E=b();var L=document.getElementsByTagName(\u0026#34;*\u0026#34;),A=L.length,O,M;var _=null;for(O=0;O\u0026lt;A;O++){M=L[O];if(m(M)){if(S(M)){_=M;break}}}if(M===null){console.warn(\u0026#34;Could not find a node of the right size. Please try a different page.\u0026#34;);return}h();x();var D=[];for(O=0;O\u0026lt;A;O++){M=L[O];if(m(M)){D.push(M)}}})() 以上两种差不多,我个人用的是第二种。具体用法为在 /layouts/partials/extend_head.html 中加入如下代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;!-- High 一下 --\u0026gt; \u0026lt;style\u0026gt; html .pull-right{ position: absolute; right: 0; top: 70px; transform-origin: 100% 100%; /* 从右下开始变换 */ } \u0026lt;/style\u0026gt; \u0026lt;div class=\u0026#34;pull-right\u0026#34;, style=\u0026#34;z-index:999\u0026#34;\u0026gt; \u0026lt;a title=\u0026#34;把这个链接拖到你的Chrome收藏夹工具栏中\u0026#34; href=\u0026#39;javascript:(function(){function h(){var e=document.createElement(\u0026#34;link\u0026#34;);e.setAttribute(\u0026#34;type\u0026#34;,\u0026#34;text/css\u0026#34;);e.setAttribute(\u0026#34;rel\u0026#34;,\u0026#34;stylesheet\u0026#34;);e.setAttribute(\u0026#34;href\u0026#34;,l);e.setAttribute(\u0026#34;class\u0026#34;,c);document.body.appendChild(e)}function p(){var e=document.getElementsByClassName(c);for(var t=0;t\u0026lt;e.length;t++){document.body.removeChild(e[t])}}function d(){var e=document.createElement(\u0026#34;div\u0026#34;);e.setAttribute(\u0026#34;class\u0026#34;,f);document.body.appendChild(e);setTimeout(function(){document.body.removeChild(e)},100)}function v(e){return{height:e.offsetHeight,width:e.offsetWidth}}function m(i){var s=v(i);return s.height\u0026gt;e\u0026amp;\u0026amp;s.height\u0026lt;n\u0026amp;\u0026amp;s.width\u0026gt;t\u0026amp;\u0026amp;s.width\u0026lt;r}function g(e){var t=e;var n=0;while(!!t){n+=t.offsetTop;t=t.offsetParent}return n}function y(){var e=document.documentElement;if(!!window.innerWidth){return window.innerHeight}else if(e\u0026amp;\u0026amp;!isNaN(e.clientHeight)){return e.clientHeight}return 0}function b(){if(window.pageYOffset){return window.pageYOffset}return Math.max(document.documentElement.scrollTop,document.body.scrollTop)}function S(e){var t=g(e);return t\u0026gt;=E\u0026amp;\u0026amp;t\u0026lt;=w+E}function x(){var e=document.createElement(\u0026#34;audio\u0026#34;);e.setAttribute(\u0026#34;class\u0026#34;,c);e.src=i;e.loop=false;var t=false,n=false,r=false;e.addEventListener(\u0026#34;timeupdate\u0026#34;,function(){var i=e.currentTime,s=D,o=s.length,u;if(i\u0026gt;=.5\u0026amp;\u0026amp;!t){t=true;T(_)}if(i\u0026gt;=15.5\u0026amp;\u0026amp;!n){n=true;k();d();for(u=0;u\u0026lt;o;u++){N(s[u])}}if(e.currentTime\u0026gt;=28.4\u0026amp;\u0026amp;!r){r=true;C()}},true);e.addEventListener(\u0026#34;ended\u0026#34;,function(){k();p()},true);e.innerHTML=\u0026#34;\u0026lt;p\u0026gt;If you are reading this, it is because your browser does not support the audio element. We recommend that you get a new browser.\u0026lt;/p\u0026gt;\u0026#34;;document.body.appendChild(e);e.play()}function T(e){e.className+=\u0026#34; \u0026#34;+s+\u0026#34; \u0026#34;+u}function N(e){e.className+=\u0026#34; \u0026#34;+s+\u0026#34; \u0026#34;+a[Math.floor(Math.random()*a.length)]}function C(){var e=document.getElementsByClassName(s);for(var t=0;t\u0026lt;e.length;){e[t].className=e[t].className.replace(s,o)}s=o}function k(){var e=document.getElementsByClassName(s);var t=new RegExp(\u0026#34;\\\\b\u0026#34;+s+\u0026#34;\\\\b\u0026#34;);for(var n=0;n\u0026lt;e.length;){e[n].className=e[n].className.replace(t,\u0026#34;\u0026#34;)}}var e=30;var t=30;var n=350;var r=350;var i=\u0026#34;//s3.amazonaws.com/moovweb-marketing/playground/harlem-shake.ogg\u0026#34;;var s=\u0026#34;mw-harlem_shake_me\u0026#34;;var o=\u0026#34;mw-harlem_shake_slow\u0026#34;;var u=\u0026#34;im_first\u0026#34;;var a=[\u0026#34;im_drunk\u0026#34;,\u0026#34;im_baked\u0026#34;,\u0026#34;im_trippin\u0026#34;,\u0026#34;im_blown\u0026#34;];var f=\u0026#34;mw-strobe_light\u0026#34;;var l=\u0026#34;//s3.amazonaws.com/moovweb-marketing/playground/harlem-shake-style.css\u0026#34;;var c=\u0026#34;mw_added_css\u0026#34;;var w=y();var E=b();var L=document.getElementsByTagName(\u0026#34;*\u0026#34;),A=L.length,O,M;var _=null;for(O=0;O\u0026lt;A;O++){M=L[O];if(m(M)){if(S(M)){_=M;break}}}if(M===null){console.warn(\u0026#34;Could not find a node of the right size. Please try a different page.\u0026#34;);return}h();x();var D=[];for(O=0;O\u0026lt;A;O++){M=L[O];if(m(M)){D.push(M)}}})()\u0026#39;\u0026gt;High 一下!\u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; 其中的 href= 的内容替换为上述两种的其中一种即可。\n2024-04-03 更新 原来脚本所请求的存储在亚马逊云服务上的音频文件和 css 文件现在获取不到了,导致这个脚本执行报错。可以去 web archive 上找到这两个链接的存档,替换即可。替换后的代码为:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 \u0026lt;!-- High 一下 --\u0026gt; \u0026lt;style\u0026gt; html .pull-right{ position: absolute; right: 0; top: 70px; transform-origin: 100% 100%; /* 从右下开始变换 */ } \u0026lt;/style\u0026gt; \u0026lt;div class=\u0026#34;pull-right\u0026#34;, style=\u0026#34;z-index:999\u0026#34;\u0026gt; \u0026lt;a title=\u0026#34;把这个链接拖到你的Chrome收藏夹工具栏中\u0026#34; href=\u0026#34; javascript: (function () { function h() { var e = document.createElement(\u0026#39;link\u0026#39;); e.setAttribute(\u0026#39;type\u0026#39;, \u0026#39;text/css\u0026#39;); e.setAttribute(\u0026#39;rel\u0026#39;, \u0026#39;stylesheet\u0026#39;); e.setAttribute(\u0026#39;href\u0026#39;, l); e.setAttribute(\u0026#39;class\u0026#39;, c); document.body.appendChild(e); } function p() { var e = document.getElementsByClassName(c); for (var t = 0; t \u0026lt; e.length; t++) { document.body.removeChild(e[t]); } } function d() { var e = document.createElement(\u0026#39;div\u0026#39;); e.setAttribute(\u0026#39;class\u0026#39;, f); document.body.appendChild(e); setTimeout(function () { document.body.removeChild(e); }, 100); } function v(e) { return { height: e.offsetHeight, width: e.offsetWidth }; } function m(i) { var s = v(i); return s.height \u0026gt; e \u0026amp;\u0026amp; s.height \u0026lt; n \u0026amp;\u0026amp; s.width \u0026gt; t \u0026amp;\u0026amp; s.width \u0026lt; r; } function g(e) { var t = e; var n = 0; while (!!t) { n += t.offsetTop; t = t.offsetParent; } return n; } function y() { var e = document.documentElement; if (!!window.innerWidth) { return window.innerHeight; } else if (e \u0026amp;\u0026amp; !isNaN(e.clientHeight)) { return e.clientHeight; } return 0; } function b() { if (window.pageYOffset) { return window.pageYOffset; } return Math.max(document.documentElement.scrollTop, document.body.scrollTop); } function S(e) { var t = g(e); return t \u0026gt;= E \u0026amp;\u0026amp; t \u0026lt;= w + E; } function x() { var e = document.createElement(\u0026#39;audio\u0026#39;); e.setAttribute(\u0026#39;class\u0026#39;, c); e.src = i; e.loop = false; var t = false, n = false, r = false; e.addEventListener(\u0026#39;timeupdate\u0026#39;, function () { var i = e.currentTime, s = D, o = s.length, u; if (i \u0026gt;= 0.5 \u0026amp;\u0026amp; !t) { t = true; T(_); } if (i \u0026gt;= 15.5 \u0026amp;\u0026amp; !n) { n = true; k(); d(); for (u = 0; u \u0026lt; o; u++) { N(s[u]); } } if (e.currentTime \u0026gt;= 28.4 \u0026amp;\u0026amp; !r) { r = true; C(); } }, true); e.addEventListener(\u0026#39;ended\u0026#39;, function () { k(); p(); }, true); e.innerHTML = \u0026#39;\u0026lt;p\u0026gt;If you are reading this, it is because your browser does not support the audio element. We recommend that you get a new browser.\u0026lt;/p\u0026gt;\u0026#39;; document.body.appendChild(e); e.play(); } function T(e) { e.className += \u0026#39; \u0026#39; + s + \u0026#39; \u0026#39; + u; } function N(e) { e.className += \u0026#39; \u0026#39; + s + \u0026#39; \u0026#39; + a[Math.floor(Math.random() * a.length)]; } function C() { var e = document.getElementsByClassName(s); for (var t = 0; t \u0026lt; e.length;) { e[t].className = e[t].className.replace(s, o); } s = o; } function k() { var e = document.getElementsByClassName(s); var t = new RegExp(\u0026#39;\\\\b\u0026#39; + s + \u0026#39;\\\\b\u0026#39;); for (var n = 0; n \u0026lt; e.length;) { e[n].className = e[n].className.replace(t, \u0026#39;\u0026#39;); } } var e = 30; var t = 30; var n = 350; var r = 350; var i = \u0026#39;//web.archive.org/web/20240301073522/s3.amazonaws.com/moovweb-marketing/playground/harlem-shake.mp3\u0026#39;; var s = \u0026#39;mw-harlem_shake_me\u0026#39;; var o = \u0026#39;mw-harlem_shake_slow\u0026#39;; var u = \u0026#39;im_first\u0026#39;; var a = [ \u0026#39;im_drunk\u0026#39;, \u0026#39;im_baked\u0026#39;, \u0026#39;im_trippin\u0026#39;, \u0026#39;im_blown\u0026#39; ]; var f = \u0026#39;mw-strobe_light\u0026#39;; var l = \u0026#39;//web.archive.org/web/20230212043533/s3.amazonaws.com/moovweb-marketing/playground/harlem-shake-style.css\u0026#39;; var c = \u0026#39;mw_added_css\u0026#39;; var w = y(); var E = b(); var L = document.getElementsByTagName(\u0026#39;*\u0026#39;), A = L.length, O, M; var _ = null; for (O = 0; O \u0026lt; A; O++) { M = L[O]; if (m(M)) { if (S(M)) { _ = M; break; } } } if (M === null) { console.warn(\u0026#39;Could not find a node of the right size. Please try a different page.\u0026#39;); return; } h(); x(); var D = []; for (O = 0; O \u0026lt; A; O++) { M = L[O]; if (m(M)) { D.push(M); } } }()); \u0026#34;\u0026gt;High 一下!\u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; 修改目录位置到文章页左边 根据 sulvblog 的方法来改。\n修改 toc.html 先修改 layouts/partials/toc.html 的代码,替换为:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 {{- $headers := findRE \u0026#34;\u0026lt;h[1-6].*?\u0026gt;(.|\\n])+?\u0026lt;/h[1-6]\u0026gt;\u0026#34; .Content -}} {{- $has_headers := ge (len $headers) 1 -}} {{- if $has_headers -}} \u0026lt;aside id=\u0026#34;toc-container\u0026#34; class=\u0026#34;toc-container wide\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;toc\u0026#34;\u0026gt; \u0026lt;details {{if (.Param \u0026#34;TocOpen\u0026#34;) }} open{{ end }}\u0026gt; \u0026lt;summary accesskey=\u0026#34;c\u0026#34; title=\u0026#34;(Alt + C)\u0026#34;\u0026gt; \u0026lt;span class=\u0026#34;details\u0026#34;\u0026gt;{{- i18n \u0026#34;toc\u0026#34; | default \u0026#34;Table of Contents\u0026#34; }}\u0026lt;/span\u0026gt; \u0026lt;/summary\u0026gt; \u0026lt;div class=\u0026#34;inner\u0026#34;\u0026gt; {{- $largest := 6 -}} {{- range $headers -}} {{- $headerLevel := index (findRE \u0026#34;[1-6]\u0026#34; . 1) 0 -}} {{- $headerLevel := len (seq $headerLevel) -}} {{- if lt $headerLevel $largest -}} {{- $largest = $headerLevel -}} {{- end -}} {{- end -}} {{- $firstHeaderLevel := len (seq (index (findRE \u0026#34;[1-6]\u0026#34; (index $headers 0) 1) 0)) -}} {{- $.Scratch.Set \u0026#34;bareul\u0026#34; slice -}} \u0026lt;ul\u0026gt; {{- range seq (sub $firstHeaderLevel $largest) -}} \u0026lt;ul\u0026gt; {{- $.Scratch.Add \u0026#34;bareul\u0026#34; (sub (add $largest .) 1) -}} {{- end -}} {{- range $i, $header := $headers -}} {{- $headerLevel := index (findRE \u0026#34;[1-6]\u0026#34; . 1) 0 -}} {{- $headerLevel := len (seq $headerLevel) -}} {{/* get id=\u0026#34;xyz\u0026#34; */}} {{- $id := index (findRE \u0026#34;(id=\\\u0026#34;(.*?)\\\u0026#34;)\u0026#34; $header 9) 0 }} {{- /* strip id=\u0026#34;\u0026#34; to leave xyz, no way to get regex capturing groups in hugo */ -}} {{- $cleanedID := replace (replace $id \u0026#34;id=\\\u0026#34;\u0026#34; \u0026#34;\u0026#34;) \u0026#34;\\\u0026#34;\u0026#34; \u0026#34;\u0026#34; }} {{- $header := replaceRE \u0026#34;\u0026lt;h[1-6].*?\u0026gt;((.|\\n])+?)\u0026lt;/h[1-6]\u0026gt;\u0026#34; \u0026#34;$1\u0026#34; $header -}} {{- if ne $i 0 -}} {{- $prevHeaderLevel := index (findRE \u0026#34;[1-6]\u0026#34; (index $headers (sub $i 1)) 1) 0 -}} {{- $prevHeaderLevel := len (seq $prevHeaderLevel) -}} {{- if gt $headerLevel $prevHeaderLevel -}} {{- range seq $prevHeaderLevel (sub $headerLevel 1) -}} \u0026lt;ul\u0026gt; {{/* the first should not be recorded */}} {{- if ne $prevHeaderLevel . -}} {{- $.Scratch.Add \u0026#34;bareul\u0026#34; . -}} {{- end -}} {{- end -}} {{- else -}} \u0026lt;/li\u0026gt; {{- if lt $headerLevel $prevHeaderLevel -}} {{- range seq (sub $prevHeaderLevel 1) -1 $headerLevel -}} {{- if in ($.Scratch.Get \u0026#34;bareul\u0026#34;) . -}} \u0026lt;/ul\u0026gt; {{/* manually do pop item */}} {{- $tmp := $.Scratch.Get \u0026#34;bareul\u0026#34; -}} {{- $.Scratch.Delete \u0026#34;bareul\u0026#34; -}} {{- $.Scratch.Set \u0026#34;bareul\u0026#34; slice}} {{- range seq (sub (len $tmp) 1) -}} {{- $.Scratch.Add \u0026#34;bareul\u0026#34; (index $tmp (sub . 1)) -}} {{- end -}} {{- else -}} \u0026lt;/ul\u0026gt; \u0026lt;/li\u0026gt; {{- end -}} {{- end -}} {{- end -}} {{- end }} \u0026lt;li\u0026gt; \u0026lt;a href=\u0026#34;#{{- $cleanedID -}}\u0026#34; aria-label=\u0026#34;{{- $header | plainify -}}\u0026#34;\u0026gt;{{- $header | safeHTML -}}\u0026lt;/a\u0026gt; {{- else }} \u0026lt;li\u0026gt; \u0026lt;a href=\u0026#34;#{{- $cleanedID -}}\u0026#34; aria-label=\u0026#34;{{- $header | plainify -}}\u0026#34;\u0026gt;{{- $header | safeHTML -}}\u0026lt;/a\u0026gt; {{- end -}} {{- end -}} \u0026lt;!-- {{- $firstHeaderLevel := len (seq (index (findRE \u0026#34;[1-6]\u0026#34; (index $headers 0) 1) 0)) -}} --\u0026gt; {{- $firstHeaderLevel := $largest }} {{- $lastHeaderLevel := len (seq (index (findRE \u0026#34;[1-6]\u0026#34; (index $headers (sub (len $headers) 1)) 1) 0)) }} \u0026lt;/li\u0026gt; {{- range seq (sub $lastHeaderLevel $firstHeaderLevel) -}} {{- if in ($.Scratch.Get \u0026#34;bareul\u0026#34;) (add . $firstHeaderLevel) }} \u0026lt;/ul\u0026gt; {{- else }} \u0026lt;/ul\u0026gt; \u0026lt;/li\u0026gt; {{- end -}} {{- end }} \u0026lt;/ul\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/details\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/aside\u0026gt; \u0026lt;script\u0026gt; let activeElement; let elements; window.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, function (event) { checkTocPosition(); elements = document.querySelectorAll(\u0026#39;h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]\u0026#39;); // Make the first header active activeElement = elements[0]; const id = encodeURI(activeElement.getAttribute(\u0026#39;id\u0026#39;)).toLowerCase(); document.querySelector(`.inner ul li a[href=\u0026#34;#${id}\u0026#34;]`).classList.add(\u0026#39;active\u0026#39;); }, false); window.addEventListener(\u0026#39;resize\u0026#39;, function(event) { checkTocPosition(); }, false); window.addEventListener(\u0026#39;scroll\u0026#39;, () =\u0026gt; { // Check if there is an object in the top half of the screen or keep the last item active activeElement = Array.from(elements).find((element) =\u0026gt; { if ((getOffsetTop(element) - window.pageYOffset) \u0026gt; 0 \u0026amp;\u0026amp; (getOffsetTop(element) - window.pageYOffset) \u0026lt; window.innerHeight/2) { return element; } }) || activeElement elements.forEach(element =\u0026gt; { const id = encodeURI(element.getAttribute(\u0026#39;id\u0026#39;)).toLowerCase(); if (element === activeElement){ document.querySelector(`.inner ul li a[href=\u0026#34;#${id}\u0026#34;]`).classList.add(\u0026#39;active\u0026#39;); } else { document.querySelector(`.inner ul li a[href=\u0026#34;#${id}\u0026#34;]`).classList.remove(\u0026#39;active\u0026#39;); } }) }, false); const main = parseInt(getComputedStyle(document.body).getPropertyValue(\u0026#39;--article-width\u0026#39;), 10); const toc = parseInt(getComputedStyle(document.body).getPropertyValue(\u0026#39;--toc-width\u0026#39;), 10); const gap = parseInt(getComputedStyle(document.body).getPropertyValue(\u0026#39;--gap\u0026#39;), 10); function checkTocPosition() { const width = document.body.scrollWidth; if (width - main - (toc * 2) - (gap * 4) \u0026gt; 0) { document.getElementById(\u0026#34;toc-container\u0026#34;).classList.add(\u0026#34;wide\u0026#34;); } else { document.getElementById(\u0026#34;toc-container\u0026#34;).classList.remove(\u0026#34;wide\u0026#34;); } } function getOffsetTop(element) { if (!element.getClientRects().length) { return 0; } let rect = element.getBoundingClientRect(); let win = element.ownerDocument.defaultView; return rect.top + win.pageYOffset; } \u0026lt;/script\u0026gt; {{- end }} 调用 layouts/_default/single.html 文件中默认有 toc.html 的调用,如果未更改的话不用管,更改的话请调用:\n1 2 3 {{- if (.Param \u0026#34;ShowToc\u0026#34;) }} {{- partial \u0026#34;toc.html\u0026#34; . }} {{- end }} 修改 css 修改 css/extended/blank.css 文件,加入下方代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 :root { --nav-width: 1380px; --article-width: 650px; --toc-width: 300px; } .toc { margin: 0 2px 40px 2px; border: 1px solid var(--border); background: var(--entry); border-radius: var(--radius); padding: 0.4em; } .toc-container.wide { position: absolute; height: 100%; border-right: 1px solid var(--border); left: calc((var(--toc-width) + var(--gap)) * -1); top: calc(var(--gap) * 2); width: var(--toc-width); } .wide .toc { position: sticky; top: var(--gap); border: unset; background: unset; border-radius: unset; width: 100%; margin: 0 2px 40px 2px; } .toc details summary { cursor: zoom-in; margin-inline-start: 20px; padding: 12px 0; } .toc details[open] summary { font-weight: 500; } .toc-container.wide .toc .inner { margin: 0; } .active { font-size: 110%; font-weight: 600; } .toc ul { list-style-type: circle; } .toc .inner { margin: 0 0 0 20px; padding: 0px 15px 15px 20px; font-size: 16px; /*目录显示高度*/ max-height: 83vh; overflow-y: auto; } .toc .inner::-webkit-scrollbar-thumb { /*滚动条*/ background: var(--border); border: 7px solid var(--theme); border-radius: var(--radius); } .toc li ul { margin-inline-start: calc(var(--gap) * 0.5); list-style-type: none; } .toc li { list-style: none; font-size: 0.95rem; padding-bottom: 5px; } .toc li a:hover { color: var(--secondary); } 重新编译后即可看到目录到了左边。\n修改主题内容宽度 修改 ·assets/css/core/theme-vars.css 文件的 :root 节点中的 --main-width 我设置的是 1024px\n修改代码块 修改渲染方式 放弃主题默认的 Highlight.js,改为 Hugo 默认的 chroma 方式渲染。根据 官方说明 来改。我用的 style 为 github-dark\n设置代码块最大高度 在 assets/css/common/post-single.css 文件中找到 .post-content code 增加 max-height: 40em; 。目前设置好最大高度后会与 lineNos: true 设置冲突,会显示两个滑动条 :( ,所以就先不开它。\n修改文章列表显示的图片位置 默认显示的图片在摘要上方而且很大,一页只能显示几个,感觉体验不是很好,所以将它改为在摘要右方显示。我很喜欢 CoolShell 的文章页的显示方法,准备改成差不多样子的。\n修改 list.html 文件为 layouts/_default/list.html 。默认的图片显示代码为:\n1 2 {{- $isHidden := (site.Params.cover.hidden | default site.Params.cover.hiddenInList) }} {{- partial \u0026#34;cover.html\u0026#34; (dict \u0026#34;cxt\u0026#34; . \u0026#34;IsHome\u0026#34; true \u0026#34;isHidden\u0026#34; $isHidden) }} 位于 \u0026lt;header class=\u0026quot;entry-header\u0026quot;\u0026gt; 的上方。将其移动到 \u0026lt;div class=\u0026quot;entry-content\u0026quot;\u0026gt; 中并替换默认的 Summary 处理方式,如下所示:\n1 2 3 4 5 \u0026lt;div class=\u0026#34;entry-content\u0026#34;\u0026gt; {{- $isHidden := (site.Params.cover.hidden | default site.Params.cover.hiddenInList) }} {{- partial \u0026#34;cover.html\u0026#34; (dict \u0026#34;cxt\u0026#34; . \u0026#34;IsHome\u0026#34; true \u0026#34;isHidden\u0026#34; $isHidden) }} {{ .Summary | replaceRE \u0026#34;\\n\u0026#34; \u0026#34;\u0026lt;br\u0026gt;\u0026#34; | safeHTML }}{{ if .Truncated }}...{{ end }} \u0026lt;/div\u0026gt; 修改 cover.html 文件为 layouts/partials/cover.html 。将 所有 \u0026lt;img 块都加上 align=\u0026quot;right\u0026quot; 。完整如下所示:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 {{- with .cxt}} {{/* Apply proper context from dict */}} {{- if (and .Params.cover.image (not $.isHidden)) }} {{- $alt := (.Params.cover.alt | default .Params.cover.caption | plainify) }} \u0026lt;figure class=\u0026#34;entry-cover\u0026#34;\u0026gt; {{- $responsiveImages := (.Params.cover.responsiveImages | default site.Params.cover.responsiveImages) | default true }} {{- $addLink := (and site.Params.cover.linkFullImages (not $.IsHome)) }} {{- $pageBundleCover := (.Resources.ByType \u0026#34;image\u0026#34;).GetMatch (printf \u0026#34;*%s*\u0026#34; (.Params.cover.image)) }} {{- $globalResourcesCover := (resources.ByType \u0026#34;image\u0026#34;).GetMatch (printf \u0026#34;*%s*\u0026#34; (.Params.cover.image)) }} {{- $cover := (or $pageBundleCover $globalResourcesCover)}} {{- if $cover -}}{{/* i.e it is present in page bundle */}} {{- if $addLink }}\u0026lt;a href=\u0026#34;{{ (path.Join .RelPermalink .Params.cover.image) | absURL }}\u0026#34; target=\u0026#34;_blank\u0026#34; rel=\u0026#34;noopener noreferrer\u0026#34;\u0026gt;{{ end -}} {{- $sizes := (slice \u0026#34;360\u0026#34; \u0026#34;480\u0026#34; \u0026#34;720\u0026#34; \u0026#34;1080\u0026#34; \u0026#34;1500\u0026#34;) }} {{- $processableFormats := (slice \u0026#34;jpg\u0026#34; \u0026#34;jpeg\u0026#34; \u0026#34;png\u0026#34; \u0026#34;tif\u0026#34; \u0026#34;bmp\u0026#34; \u0026#34;gif\u0026#34;) -}} {{- if hugo.IsExtended -}} {{- $processableFormats = $processableFormats | append \u0026#34;webp\u0026#34; -}} {{- end -}} {{- $prod := (hugo.IsProduction | or (eq site.Params.env \u0026#34;production\u0026#34;)) }} {{- if (and (in $processableFormats $cover.MediaType.SubType) ($responsiveImages) (eq $prod true)) }} \u0026lt;img loading=\u0026#34;lazy\u0026#34; align=\u0026#34;right\u0026#34; srcset=\u0026#34;{{- range $size := $sizes -}} {{- if (ge $cover.Width $size) -}} {{ printf \u0026#34;%s %s\u0026#34; (($cover.Resize (printf \u0026#34;%sx\u0026#34; $size)).Permalink) (printf \u0026#34;%sw ,\u0026#34; $size) -}} {{ end }} {{- end -}}{{$cover.Permalink }} {{printf \u0026#34;%dw\u0026#34; ($cover.Width)}}\u0026#34; sizes=\u0026#34;(min-width: 768px) 720px, 100vw\u0026#34; src=\u0026#34;{{ $cover.Permalink }}\u0026#34; alt=\u0026#34;{{ $alt }}\u0026#34; width=\u0026#34;{{ $cover.Width }}\u0026#34; height=\u0026#34;{{ $cover.Height }}\u0026#34;\u0026gt; {{- else }}{{/* Unprocessable image or responsive images disabled */}} \u0026lt;img loading=\u0026#34;lazy\u0026#34; align=\u0026#34;right\u0026#34; src=\u0026#34;{{ (path.Join .RelPermalink .Params.cover.image) | absURL }}\u0026#34; alt=\u0026#34;{{ $alt }}\u0026#34;\u0026gt; {{- end }} {{- else }}{{/* For absolute urls and external links, no img processing here */}} {{- if $addLink }}\u0026lt;a href=\u0026#34;{{ (.Params.cover.image) | absURL }}\u0026#34; target=\u0026#34;_blank\u0026#34; rel=\u0026#34;noopener noreferrer\u0026#34;\u0026gt;{{ end -}} \u0026lt;img loading=\u0026#34;lazy\u0026#34; align=\u0026#34;right\u0026#34; src=\u0026#34;{{ (.Params.cover.image) | absURL }}\u0026#34; alt=\u0026#34;{{ $alt }}\u0026#34;\u0026gt; {{- end }} {{- if $addLink }}\u0026lt;/a\u0026gt;{{ end -}} {{/* Display Caption */}} {{- if not $.IsHome }} {{ with .Params.cover.caption }}\u0026lt;p\u0026gt;{{ . | markdownify }}\u0026lt;/p\u0026gt;{{- end }} {{- end }} \u0026lt;/figure\u0026gt; {{- end }}{{/* End image */}} {{- end -}}{{/* End context */ -}} 增加 css 在文件 assets/css/extended/blank.css 中增加以下代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 .entry-cover { float:right; width: 20%; margin-left: 20px; } .entry-content { margin: 20px 0; color: var(--secondary); font-size: 14px; line-height: 1.6; overflow: hidden; display: block; } 这样做完的效果就是文章列表页的摘要可以自由显示文字和图片,默认的 cover 在文字右边,文字会环绕图片显示。\n标签页词云效果 根据 Sulv's Blog 的方法来改。\n修改 terms.html 将 layouts/_default/terms.html 中的 \u0026lt;ul class=\u0026quot;terms-tags\u0026quot;\u0026gt; 代码块替换为:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 \u0026lt;ul class=\u0026#34;terms-tags\u0026#34;\u0026gt; {{- $type := .Type }} {{- range $key, $value := .Data.Terms.Alphabetical }} {{- $name := .Name }} {{- $count := .Count }} {{- with $.Site.GetPage (printf \u0026#34;/%s/%s\u0026#34; $type $name) }} \u0026lt;li\u0026gt; {{ $largestFontSize := 1.5 }} {{ $smallestFontSize := 1 }} {{ $fontSpread := sub $largestFontSize $smallestFontSize }} {{ $max := add (len (index $.Site.Taxonomies.tags.ByCount 0).Pages) 1 }} {{ $min := len (index $.Site.Taxonomies.tags.ByCount.Reverse 0).Pages }} {{ $spread := sub $max $min }} {{ $fontStep := div $fontSpread $spread }} {{ $weigth := div (sub (math.Log $count) (math.Log $min)) (sub (math.Log $max) (math.Log $min)) }} {{ $currentFontSize := (add $smallestFontSize (mul (sub $largestFontSize $smallestFontSize) $weigth)) }} \u0026lt;a href=\u0026#34;{{ .Permalink }}\u0026#34; style=\u0026#34;font-size: {{ $currentFontSize }}rem; font-weight: {{ mul $currentFontSize 200 }};\u0026#34;\u0026gt; {{ .Name }} \u0026lt;sup\u0026gt;\u0026lt;strong\u0026gt;\u0026lt;sup\u0026gt;{{ $count }}\u0026lt;/sup\u0026gt;\u0026lt;/strong\u0026gt;\u0026lt;/sup\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;/li\u0026gt; {{- end }} {{- end }} \u0026lt;/ul\u0026gt; 增加 css 在 assets/css/extended/blank.css 中增加如下代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 /*标签*/ .terms-tags { text-align: center; } .terms-tags a:hover { background: none; -moz-transform: scale(1.2); -ms-transform: scale(1.2); -o-transform: scale(1.2); transform: scale(1.3); } .terms-tags a { border-radius: 30px; background: none; transition: transform 0.5s; } .dark .terms-tags a { background: none; } .dark .terms-tags a:hover { background: none; -moz-transform: scale(1.2); -ms-transform: scale(1.2); -o-transform: scale(1.2); transform: scale(1.3); } .terms-tags li { margin: 5px; } 创建关于页 创建 about.md 文件 使用命令 hugo new content about.md 创建关于页。将 front matter 替换为如下内容:\n1 2 3 4 5 6 title: \u0026#39;关于我\u0026#39; # 显示的标题 layout: \u0026#34;about\u0026#34; # 使用的 html 模板 # url: \u0026#34;/about\u0026#34; description: \u0026#34;兴趣使然的程序员\u0026#34; # 头像下方的简短介绍 comments: true # 是否开启评论 avatar: \u0026#34;/images/avatar.gif\u0026#34; # 头像 创建 html 模板 创建 layouts/_default/about.html 文件,填入以下代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 {{ define \u0026#34;main\u0026#34; }} \u0026lt;header class=\u0026#34;page-header\u0026#34;\u0026gt; \u0026lt;h1\u0026gt;{{ .Title }}\u0026lt;/h1\u0026gt; \u0026lt;/header\u0026gt; \u0026lt;div class=\u0026#34;about-container\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;about-avatar\u0026#34;\u0026gt; \u0026lt;img src=\u0026#34;{{ .Params.avatar }}\u0026#34; alt=\u0026#34;头像\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;about-description\u0026#34;\u0026gt; {{ .Params.description }} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;post-content\u0026#34;\u0026gt; {{ .Content }} \u0026lt;/div\u0026gt; {{- if (.Param \u0026#34;comments\u0026#34;) }} {{- partial \u0026#34;comments.html\u0026#34; . }} {{- end }} {{ end }} 增加 css 在 assets/css/extended/custom.css 中增加以下代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 /* about page */ .page-header { text-align: center; } .page-header h1 { margin-bottom: 10%; /* 或者你需要的具体数值 */ } .about-container { display: flex; flex-direction: column; align-items: center; text-align: center; } .about-avatar img { border-radius: 50%; width: 150px; height: 150px; } .about-bio { margin-top: 10%; } .about-description { text-align: center; margin-top: 20px; margin-bottom: 20px; } 修改博客文章链接 参考:https://blog.lxdlam.com/post/9cc3283b/ 来改。\n修改模板 在 archetypes/default.md 中的 front matter 添加下方代码:\n1 slug: {{ substr (sha256 (printf \u0026#34;%s%s\u0026#34; .Date (replace .TranslationBaseName \u0026#34;-\u0026#34; \u0026#34; \u0026#34; | title))) 0 16 }} 这段 Hugo 模板代码用于生成一个独特且简短的标识符(或称为“slug”),用于在网站上唯一标识每个页面。这个标识符是通过对页面的发布日期和标题进行 SHA-256 哈希计算,并取哈希结果的前 16 个字符来生成的。\n修改 Hugo config 在 hugo.yaml 中加入:\n1 2 3 permalinks: page: posts: /post/:slug/ 这样以后用 hugo new 生成的文章就会有自动生成的短链接了。\n增加评论 使用 giscus 评论系统。在 layouts/partials/comments.html 文件中增加 giscus 提供的 \u0026lt;script\u0026gt; 代码块。\n修改 header 条 默认的 header 条会随着下滑消失,想修改为不会消失。\n增加 css 在 assets/css/extended/custom.css 中增加如下代码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 .header { position: fixed; top: 0; width: 100%; z-index: 1000; background-color: var(--code-bg); border-bottom: 1px solid var(--border); } .main { margin-top: 5%; } h1, h2, h3, h4, h5, h6 { scroll-margin-top: calc(var(--header-height) + 1px); } 修改目录代码(非必要) 我之前设置的目录会与这个有点显示上的冲突,具体表现为下滑一段距离后目录的顶部显示不全,如果你也用了和我一样的修改目录的方法,那你需要修改之前的目录 css 代码,位置为:assets/css/extended/blank.css 找到 .wide .toc 改为:\n1 2 3 4 5 6 7 8 9 .wide .toc { position: sticky; top: 5%; border: unset; background: unset; border-radius: unset; width: 100%; margin: 0 2px 40px 2px; } 这样就可以正常显示了。\n修改标签页列表展示形式 默认的标签页列表不会显示图片,想改为和首页一样的效果。将 assets/css/common/post-entry.css 中的\n1 2 3 .tag-entry .entry-cover { display: none; } 注释掉即可。\n渲染数学公式 根据 官方文档 来修改。将下面代码加入到 layouts/partials/extend_footer.html 中:\n1 2 3 \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML\u0026#34;\u0026gt; \u0026lt;/script\u0026gt; 这样可以渲染 $$ ...$$ 这样的数学公式,如果想使用 inline math 的话需要使用 \\\\(...\\\\)\n复杂公式渲染 上面的方法可以渲染一些简单的公式,当公式比较复杂,例如:拥有三个以上的花括号时,使用上面的方法会渲染出错。根据这篇文章 来修改,注释掉上述代码,更换为:\n1 2 3 4 5 \u0026lt;script src=\u0026#34;//yihui.org/js/math-code.js\u0026#34; defer\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;!-- Just one possible MathJax CDN below. You may use others. --\u0026gt; \u0026lt;script defer src=\u0026#34;//mathjax.rstudio.com/latest/MathJax.js?config=TeX-MML-AM_CHTML\u0026#34;\u0026gt; \u0026lt;/script\u0026gt; 写简单的公式使用方法和之前一样,当写复杂公式时,需要使用 ` 符号包裹公式,例如:\n1 2 3 4 普通公式为:$$a^2 + b^2 = c^2$$ 复杂公式为:`$$a^2 + b^2 = c^2$$` 这样即可正确渲染复杂公式\n修改表格渲染 默认的表格渲染没有列边界线,有时看着费劲。将 assets/css/common/post-single.css 中的\n1 2 3 4 5 6 .post-content table:not(.highlighttable, .highlight table, .gist .highlight) td { min-width: 80px; padding: 12px 8px; line-height: 1.5; border-bottom: 1px solid var(--border); } 修改为:\n1 2 3 4 5 6 7 .post-content table:not(.highlighttable, .highlight table, .gist .highlight) td { min-width: 80px; padding: 12px 8px; line-height: 1.5; /* border-bottom: 1px solid var(--border); */ border: 1px solid var(--border); } ","permalink":"https://blog.raikiriww.net/post/199ab6e24c79e35a/","summary":"\u003cp\u003e此篇博客介绍我的博客的修改内容和方法。我的主题为 \u003ca href=\"https://github.com/adityatelange/hugo-PaperMod\"\u003ehugo-PaperMod\u003c/a\u003e。我修改前都将博客内的资源文件夹 (assets, i18n, lay) 复制到了项目根目录中,这样以后更新主题也不会导致修改消失。。\u003c/p\u003e","title":"自定义博客"},{"content":"使用 Hugo 搭建博客。\n安装 Hugo 根据 Hugo 官网 的文档来安装 Hugo。\n安装主题 我使用的主题为:hugo-PaperMod ,根据主题文档安装主题(推荐使用 submodule 的方式安装)。将默认的配置文件 hugo.toml 的后缀改为 yaml,方便使用。\n主题配置 复制主题网站给的 Sample config.yml 的内容到 hugo.yaml 中,额外增加:\n1 2 languageCode: zh defaultContentLanguage: zh 将网站语言设置为中文。\n默认文章配置 复制 Sample Page.md 的内容到 archetypes/default.md 中(title 和 date 不覆盖,只将 toml 格式的配置改为 yaml 格式即可)。可以省去自己一个一个寻找增加的时间,当然,具体内容还是要根据自己的要求来改。\n写文章 使用命令 hugo new content posts/my-first-post.md 来新建一篇文章。\n本地预览 使用命令 hugo server -D 预览生成好的网页。\n","permalink":"https://blog.raikiriww.net/post/2d1d3327b3665269/","summary":"\u003cp\u003e使用 Hugo 搭建博客。\u003c/p\u003e","title":"搭建博客"},{"content":"公司有一个同事做的项目,其中有一个 Python 写的程序会反复降低 CPU 的电压直至死机重启,程序会在降压前保存本次的数据。听起来很合理,先保存数据再降低电压,如果死机了导致重启,那上次的数据也保存到本地了。但在 windows 电脑上实际运行时,每次程序导致 windows 死机重启后,保存的数据文件都为空。他没搞定这个就离职了,于是我就接手来查这个 bug 了。\n查找问题 大概的代码示例如下:\n1 2 3 4 5 6 7 # 写入文件 with open(file_path, \u0026#34;w\u0026#34;) as f: f.write(some_data) logging.warning(\u0026#34;Write to checkpoint file\u0026#34;) # 降低 CPU 电压,过低会导致死机 set_voltage_offset(v_off) 从代码结构来看确实没什么问题,是先保存数据再降低电压,即使后边的操作导致死机也是在写入操作完成后,应该不会影响保存的数据才对。但事实是确实有影响,在 windows 上测试了好几次保存的数据都为空。with open() 语句是 Python 中常用的文件操作语句,不应该会导致写入异常,于是怀疑是降压操作导致的。\n分析问题 在 Python 中,当使用 with open() 语句来写入文件时,它会负责管理文件的打开和关闭,通常情况下, with 语句块结束后,Python 会自动关闭文件,并确保所有数据写入硬盘。但,这个操作不是立即发生的。\n当写入文件时,操作系统通常会缓存这些操作,以便一次性的将多个写入操作合并,从而提高效率。这意味着即使 Python 代码执行了写入操作(也就是 write()),也不能保证这些数据已经永久的保存到了硬盘上。如果在 with 语句块结束后立即死机,这些数据可能会丢失。\n解决方法 现在知道了导致数据保存失败的原因是出在 windows 的系统缓存机制上,那只要找到方法可以强制系统将缓存的数据写入硬盘就好了。修改后的代码如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 写入文件 with open(file_path, \u0026#34;w\u0026#34;) as f: f.write(some_data) # 确保数据从 Python 的内部缓冲区写入操作系统的缓冲区 f.flush() # 确保数据从操作系统的缓冲区写入磁盘 os.fsync(f.fileno()) logging.warning(\u0026#34;Write to checkpoint file\u0026#34;) # 降低 CPU 电压,过低会导致死机 set_voltage_offset(v_off) 新增了两行代码。\nf.flush() 的作用为刷新 Python 的内部缓冲区,确保所有数据写入操作系统的缓冲区。但这个并不能保证操作系统会立刻将数据写入硬盘。 os.fsync(f.fileno()) 的作用为强制操作系统将其缓冲区的数据写入硬盘。这样就保证了如果之后的代码导致系统死机,这部分数据也会完整的保存在硬盘上。 将修改后的程序在 windows 上测试,数据每次都会完整的保存在硬盘上。\n后记 在 Microsoft 的一篇官方文档中有提到 disk write caching ,也就是写入缓存,并给出了关闭的方法。\n关于 disk write caching ,官方的描述为:\nAdditionally, turning disk write caching on may increase operating system performance; however, it may also result in the loss of information if a power failure, equipment failure, or software failure occurs.\n确实与我遇到的情况一样。\n还有一篇更详细一点的介绍:https://learn.microsoft.com/en-US/windows/client-management/client-tools/change-default-removal-policy-external-storage-media\n总的来说 disk write caching 在一般情况下可以提高性能。但在需要确保极端情况下写入数据完整性时,可以考虑关闭或者手动强制写入。\n","permalink":"https://blog.raikiriww.net/post/139dae6586b8dfe0/","summary":"\u003cp\u003e公司有一个同事做的项目,其中有一个 Python 写的程序会反复降低 CPU 的电压直至死机重启,程序会在降压前保存本次的数据。听起来很合理,先保存数据再降低电压,如果死机了导致重启,那上次的数据也保存到本地了。但在 windows 电脑上实际运行时,每次程序导致 windows 死机重启后,保存的数据文件都为空。他没搞定这个就离职了,于是我就接手来查这个 bug 了。\u003c/p\u003e","title":"Python 写入文件失败"},{"content":"网络信息 Github: https://github.com/raikiriww\nEmail: [email protected]\n","permalink":"https://blog.raikiriww.net/about/","summary":"网络信息 Github: https://github.com/raikiriww\nEmail: [email protected]","title":"关于我"}]