跳到主要内容
Jason Lee
The owner of this blog site

I'm a programmer who loves to learn new technologies and build cool stuff. I'm currently working as a software engineer at a small company in Shanghai. Hope to find some connections with you!

View All Authors

基于eggjs, sequelize和pgsql的小需求

· 阅读需 5 分钟
Jason Lee
The owner of this blog site

前言

这是我在部门遇到的第一个小需求。这一次,我写的是后端代码。刚开始,我还陷入在一种思维定式里,似乎一定要在现有项目的工程里添加代码。如果这样做,就需要在本地或者测试服务器启动项目,然而不熟悉服务器环境和配置的我,容易卡在一个环节上无法继续。我后面想明白了,完全可以将其视作一个独立的小项目。

pgsql 启动入口

不同于 mysql 直接通过命令行工具启动,pgsql 的启动方式有两种。一种是自带的 pgadmin4 这款数据库可视化软件,只要第一次连接上,后续打开都会自动连接。另一种是 sql shell, 也是随 pgsql 一起安装的。打开后,前面几个参数都是默认,最后输入口令(密码)。

16959671454581695967145365.png

eggjs+sequelize 的开发流程

初始化 egg 项目和安装 npm 包

$ npm init egg --type=simple
$ npm i

# sequelize 和 nodejs的pgsql驱动
npm install --save egg-sequelize pg pg-hstore

在 config/plugin.js 中引入 egg-sequelize 插件

'use strict';

exports.sequelize = {
enable: true,
package: 'egg-sequelize',
};

在 config/config.default.js 中编写 sequelize 配置

/* eslint valid-jsdoc: "off" */

'use strict';

module.exports = appInfo => {
/**
* built-in config
* @type {Egg.EggAppConfig}
**/
const config = exports = {};

// use for cookie sign key, should change to your own and keep security
config.keys = appInfo.name + '_1696034448361_9424';

// add your middleware config here
config.middleware = [];

// mysql

// config.sequelize = {
// dialect: 'mysql', // support: mysql, mariadb, postgres, mssql
// database: 'egg-sequelize-example-dev',
// host: '127.0.0.1',
// port: 3306,
// username: 'root',
// password: 'root',
// };

// pgsql

config.sequelize = {
database: 'egg-sequelize-example-dev',
username: 'postgres',
password: 'root',
host: 'localhost', // 或者你的数据库主机地址
port: 5432, // 或者你的数据库端口号
dialect: 'postgres', // 指定数据库类型为PostgreSQL
};

// add your user config here
const userConfig = {
// myAppName: 'egg',
};

return {
...config,
...userConfig,
};
};

创建数据库和数据表(在 pgadmin4 等工具中写 sql 语句)

CREATE DATABASE IF NOT EXISTS 'egg-sequelize-doc-default';
create table [name];

编写 model

注意字段要和数据表中定义的完全一致。这时候,model 可以通过 ctx.model 访问到,在编写 controller 或 service 层时可通过 ide 检查能否找到 model,若不能,检查 typings 文件夹下相应的 index.d.ts 文件,看看名称能否对应上。

# app/model/station.js

'use strict';

module.exports = (app) => {
const { STRING, INTEGER, TEXT, DATE } = app.Sequelize;
const { DataTypes } = require('sequelize');

const Station = app.model.define('Station', {
id: { type: INTEGER, primaryKey: true, autoIncrement: true },
name: { type: STRING(30), allowNull: false },
associated_yard: { type: STRING(30), allowNull: false },
address: { type: STRING(50) },
longitude: { type: DataTypes.NUMERIC(5), allowNull: false},
latitude: { type: DataTypes.NUMERIC(5), allowNull: false },
province: {type: STRING(32), allowNull: false},
city: {type: STRING(32), allowNull: false},
county: { type: STRING(32), allowNull: false },
tag: { type: DataTypes.ARRAY(DataTypes.TEXT) },
property_unit: { type: STRING(30) },
property_contact: { type: STRING(30) },
property_phone: { type: STRING(11) },
status: { type: STRING(10), allowNull: false },
map_induction: { type: DataTypes.BOOLEAN, allowNull: false },
area: { type: STRING(30) },
accounting_policy: { type: STRING(30), allowNull: false },
operating_description: { type: TEXT, allowNull: false },
comment: { type: TEXT },
created_at: DATE,
updated_at: DATE,
});

return Station;
};

上述代码中需要注意 sequelize 的表名推导问题。在const Station = app.model.define('Station', {...})中,第一个 Station 是模型名称,是 eggjs 中调用 app.model.[name]用到的名称。然后 define 函数中的'Station'和数据库中的表明存在对应关系,具体的对应规则是:

  1. 默认的是单数转复数:默认情况下,Sequelize 会将模型的名称转换为复数形式,并将其用作数据库表的名称。比如,station 转成 stations。所以创建表时,应该把表命名为复数形式。
  2. 自定义表名:可以在定义模型时明确指定要映射到的表的名称。
const User = sequelize.define('User', {
// 模型属性定义
}, {
tableName: 'custom_users_table' // 自定义表名
});

编写 controller 和 service 层,在 router.js 中添加路由。

sklearn crash course

· 阅读需 4 分钟
Jason Lee
The owner of this blog site

前言

油管视频教程。想到哪儿写到哪儿。

如何让 jupyter notebook 切换到 venv 环境

首先在虚拟环境下的 cmd 中运行:

pip install ipykernel

然后,在在虚环境中将当前的虚拟环境添加到 Jupyter Notebook 的 kernel 中:

python -m ipykernel install --name 虚环境名称 --display-name 虚环境名称 --user

框架

16955688583221695568857524.png

上图是用 sklearn 完成机器学习任务的一般框架。首先数据类型分为特征(X)和标签(y)。pipeline(管道、流程)包含数据归一化(preprocessing 的一种)和模型(model)。pipeline 有两个重要 api:fit 和 predict,前者训练,后者测试。

如果要确定超参数的值,且训练数据有限的情况下,需要用交叉验证(Cross validation, CV)。用法是:

mod = GridSearchCV(estimator=pipe, param_grid={
'model__n_neighbors': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
},
cv=3)

其中,estimator 是 pipeline 或者 model,param_grid 是需要确定的超参数,是一个字典,键是'model__[name]'的格式,cv 表示要将数据分为几折来进行交叉验证。

完整代码如下:

from sklearn.datasets import load_boston
from sklearn.neighbors import KNeighborsRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import GridSearchCV
import pandas as pd

mod = GridSearchCV(estimator=pipe, param_grid={
'model__n_neighbors': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
},
cv=3)
mod.fit(X, y);

## 查看交叉验证的结果
pd.DataFrame(mod.cv_results_)

预处理

视频以一个存在离群点的场景为例,说明预处理措施的作用。离群点会导致模型预测的偏差。

16956472633051695647262551.png

最容易想到的预处理方式是正态分布化,但是离群点问题仍然存在。因此,采用一种均匀分布方法:QuantileTransformer。

16956474313051695647430838.png

可以看出,离群点不明显了。

另外,增加特征的维数(PolynomialFeatures)也在预处理范畴内。增加特征维数可以更好地捕获数据中的特征关系。

最后介绍了 One Hot Encoding,一种经典的将文本数据转为数值特征(标签)的预处理措施。

总结一下,sklearn 中,调用预处理措施的格式是:Transformer().fit_transform(data).

指标

precision_score:分母是模型预测为正类的个数,分子是模型预测正类正确的个数。

recall_score: 分母是样本中所有正类的个数,分子是分母中模型预测正类正确的个数。

GridSearchCV (或者 model 和 pipeline)可以使用 scoring 来自定义目标函数。而自定义的损失函数需要 make_scorer 生成。

from sklearn.model_selection import GridSearchCV
from sklearn.metrics import precision_score, recall_score, make_scorer

def min_recall_precision(est, X, y_true, sample_weight=None):
y_pred = est.predict(X)
recall = recall_score(y_true, y_pred)
precision = precision_score(y_true, y_pred)
return min(recall, precision)

grid = GridSearchCV(
estimator=LogisticRegression(max_iter=1000),
param_grid={'class_weight': [{0: 1, 1: v} for v in np.linspace(1, 20, 30)]},
scoring={'precision': make_scorer(precision_score),
'recall': make_scorer(recall_score),
'min_both': min_recall_precision},
refit='min_both',
return_train_score=True,
cv=10,
n_jobs=-1
)

kafka初体验

· 阅读需 4 分钟
Jason Lee
The owner of this blog site

启动 zookeeper 和 kafka

安装参考教程。本机版本是 2.12-3.5.1。

在启动 kafka 之前先启动 zookeeper:

.\bin\windows\zookeeper-server-start.bat .\config\zookeeper.properties

然后

.\bin\windows\kafka-server-start.bat .\config\server.properties

创建 topic

.\bin\windows\kafka-topics.bat --create --bootstrap-server localhost:9092 --topic my-topic --partitions 3 --replication-factor 1

解释一下参数的含义:

  • kafka-topics.bat:用于创建、列出或删除 Kafka 主题的命令。
  • create:指示要创建主题。
  • bootstrap-server localhost:9092:指定 Kafka 服务器的地址和端口。确保这个地址和端口与你的 Kafka 服务器配置一致。
  • topic my-topic:指定要创建的主题名称,你可以将 my-topic 替换为你想要的主题名称。
  • partitions 3:指定主题的分区数。你可以根据需要调整分区数。
  • replication-factor 1:指定主题的复制因子。在本例中,复制因子为 1,这意味着每个分区只有一个副本。

列出 kafka 所有 topics,查看是否创建成功:

.\bin\windows\kafka-topics.bat --list --bootstrap-server localhost:9092

生产者发送消息

.\bin\windows\kafka-console-producer.bat --bootstrap-server localhost:9092 --topic my-topic
  • kafka-console-producer.bat:这是 Kafka 的命令行生产者工具。
  • bootstrap-server localhost:9092:指定 Kafka 服务器的地址和端口。确保这个地址和端口与你的 Kafka 服务器配置一致。
  • topic your-topic-name:指定你要发送消息的主题名称,请将 your-topic-name 替换为你的实际主题名称。

运行上述命令后,命令行将进入消息输入模式。你可以在命令行中输入消息,然后按 Enter 键发送它们。当你完成发送消息后,你可以通过键入 Ctrl+C 来退出 Kafka 生产者。

消费者接受消息

.\bin\windows\kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic my-topic --from-beginning
  • kafka-console-consumer.bat:这是 Kafka 的命令行消费者工具。
  • bootstrap-server localhost:9092:指定 Kafka 服务器的地址和端口。确保这个地址和端口与你的 Kafka 服务器配置一致。
  • topic your-topic-name:指定你要消费消息的主题名称,请将 your-topic-name 替换为你的实际主题名称。
  • from-beginning:这个选项表示你要从主题的起始位置开始消费消息。如果你希望从当前的消息位置开始消费,可以省略这个选项。

运行上述命令后,Kafka 消费者将开始消费指定主题中的消息。如果你之前已经在生产者中发送了消息到该主题,那么这些消息将会显示在命令行中。

你可以在命令行中看到消费的消息,直到你手动停止消费者(通过按 Ctrl+C)或者关闭命令行窗口。

文本分类比赛学习记录

· 阅读需 6 分钟
Jason Lee
The owner of this blog site

前言

本文是我对在公司参加的“ChatGPT 生成文本检测器”比赛。数据集为中文作文样本,其中从互联网上采集得到了真实作文,并且 ChatGLM-6B 生成了部分作文。参赛选手的任务是根据文本内容,区分作文的来源。但是,文本不是以内容呈现,而是一堆数字字符串,形如:[0 43 2 66]。可以推测出,每个数字代表一个汉字在语料库中的索引。

文本分类任务的四步:

  • 准备数据集:包括加载数据集和执行基本预处理,然后把数据集分为训练集和验证集。
  • 特征工程:将原始数据集被转换为用于训练机器学习模型的平坦特征(flat features)。
  • 模型训练
  • 进一步提高分类器性能

下面按照前面三步(省略第四步)介绍我的做法。

准备数据集

这一步的主要工作是读取原始文件,并划分训练集和验证集,用到的库 pandas 和 sklearn。

import pandas as pd
from sklearn import model_selection, preprocessing, naive_bayes, metrics, linear_model
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer

df = pd.read_csv('train.csv')

# print(df['content'])

#将数据集分为训练集和验证集
train_x, valid_x, train_y, valid_y = model_selection.train_test_split(df['content'], df['label'])

# label编码为目标变量
encoder = preprocessing.LabelEncoder()
train_y = encoder.fit_transform(train_y)
valid_y = encoder.fit_transform(valid_y)

特征工程

计数向量是数据集的矩阵表示,其中每行代表来自语料库的文档,每列表示来自语料库的术语,并且每个单元格表示特定文档中特定术语的频率计数:

#创建一个向量计数器对象
count_vect = CountVectorizer(analyzer='word', token_pattern=r'\w{1,}')
count_vect.fit(df['content'])

#使用向量计数器对象转换训练集和验证集
xtrain_count = count_vect.transform(train_x)
xvalid_count = count_vect.transform(valid_x)

模型训练

线性分类器用于训练。

## 训练函数
def train_model(classifier, feature_vector_train, label, feature_vector_valid, is_neural_net=False):
# fit the training dataset on the classifier
classifier.fit(feature_vector_train, label)


# predict the labels on validation dataset
predictions = classifier.predict(feature_vector_valid)


if is_neural_net:
predictions = predictions.argmax(axis=-1)


return classifier, metrics.accuracy_score(predictions, valid_y)

if __name__ == "__main__":
# classifier, accuracy = train_model(naive_bayes.MultinomialNB(), xtrain_count, train_y, xvalid_count)
# print("NB, Count Vectors: ", accuracy)

classifier, accuracy = train_model(linear_model.LogisticRegression(), xtrain_count, train_y, xvalid_count)
print("LR, Count Vectors: ", accuracy)

# csv out

testDF = pd.read_csv('test.csv')
test_x = testDF['content']

#使用向量计数器对象转换测试集
xtest_count = count_vect.transform(test_x)

predictions = classifier.predict(xtest_count)

submissionDF = pd.DataFrame()
submissionDF['name'] = testDF['name']
submissionDF['label'] = predictions

submissionDF.to_csv('LR_submission.csv', index=False)

总结

这次是我在 NLP 领域的一次小试牛刀,主要用到了 sklearn 框架,使得代码的编写变得简单。在不需要数据清洗等步骤的情况下,通过简单的计数向量和逻辑回归就使得最终的测试集结果达到了 0.99 以上,说明本次比赛的数据还是比较简单的。

另外,这次比赛也改变了我对于机器学习的认知。一开始我认为这是一个有难度的任务,想去 huggingface 上找一些文本分类的模型,践行“拿来主义”。但是怎么也找不到合适的。后来,在网上搜索文本分类的解决方案,看到一个相似的任务,且用了 sklearn,很短的代码就能达到不错的效果。也许,传统机器学习的能力比我想象的强很多。

更新

过了一段时间后,在接触更多 sklearn 知识后,我用 pipeline 来以更简单的方式实现:

import pandas as pd
from sklearn import model_selection, preprocessing, naive_bayes, metrics, linear_model
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.decomposition import PCA, TruncatedSVD
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.ensemble import VotingClassifier
from sklearn.neighbors import KNeighborsClassifier # 用于分类任务
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

df = pd.read_csv('train.csv')
X = df['content'].values
y = df['label'].values
le = preprocessing.LabelEncoder()
y = le.fit_transform(y)


#将数据集分为训练集和验证集
train_x, valid_x, train_y, valid_y = model_selection.train_test_split(X, y, random_state=43)

pipe_lr = Pipeline([('cv', CountVectorizer(analyzer='word', token_pattern=r'\w{1,}')),
('scl', StandardScaler(with_mean=False)),
('clf', linear_model.LogisticRegression(max_iter=50000))])

pipe_lr.fit(train_x, train_y)
print('Test Accuracy: %.4f' %pipe_lr.score(valid_x, valid_y))

## more metrics
valid_y_pred = pipe_lr.predict(valid_x)
confmat = confusion_matrix(y_true=valid_y, y_pred=valid_y_pred)
print(confmat)

print('Precision: %.3f' % precision_score(y_true=valid_y, y_pred=valid_y_pred))
print('Recall: %.3f' % recall_score(y_true=valid_y, y_pred=valid_y_pred))
print('F1: %.3f' % f1_score(y_true=valid_y, y_pred=valid_y_pred))

## draw roc
y_probs = pipe_lr.predict_proba(valid_x)
fpr, tpr, thresholds = roc_curve(valid_y, y_probs[:, 1], pos_label=1)
roc_auc = roc_auc_score(valid_y, y_probs[:, 1])
print('auc: %.4f' % roc_auc)
# Plot the ROC curve
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, color='darkorange', lw=2, label='ROC curve (area = %.4f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic')
plt.legend(loc='lower right')
plt.show()


## testset result

testDF = pd.read_csv('test.csv')
test_x = testDF['content'].values

pipe_lr.fit(X, y)
predictions = pipe_lr.predict(test_x)

submissionDF = pd.DataFrame()
submissionDF['name'] = testDF['name']
submissionDF['label'] = predictions

submissionDF.to_csv('LR_submission.csv', index=False)

在新版本中,加入了不少指标用于衡量模型性能。更重要的是,之前在输出测试结果时,用的是训练集上训练的模型,而不是整个数据集上重新训练一次。在重新训练后,发现指标从 0.9901 来到了 0.9912,提示非常显著。这提示了我以后一定要注意这个问题。

typescript文件造成win文件夹卡顿原因及解决方案

· 阅读需 2 分钟
Jason Lee
The owner of this blog site

写了好几天的 ts 代码,发现打开包含 ts 代码的文件夹时,文件夹会缓慢刷新,而且不是列表形式展示。非常痛苦:(

一开始以为是 windows11 的 bug,后来一想,最开始新建 ts 代码时,win11 直接把它当成视频文件,会不会和这个有关系。所以就用 google 搜了这个问题,果然,只有第一个搜索结果给出了问题原因和解决方案。正如所预料的,win11 把 ts 文件默认成视频文件,每次展示都要刷新。办法就是修改注册表,使系统默认 ts 为文本文档。新建.reg文件:

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\.ts]
@="txtfile"
"Content Type"="text/plain"
"PerceivedType"="text"

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\KindMap]
".ts"="document"

双击即修改完成。

参考: https://www.cnblogs.com/himeka/p/16306947.html

另外,我发现只有 google 能搜到上述解决方案,bing 和 baidu 都不行。现在知道用谁了吧...

人人都能听懂的 javascript 异步机制

· 阅读需 8 分钟
Jason Lee
The owner of this blog site

前言

同步异步一直是一个绕不过去的小问题,要么遇不上,要么遇上了似懂非懂。这篇文章力争从异步任务的底层需求出发,对 promise、async/await 进行一个详细的了解。

同步 or 异步?

16913918214981691391820745.png

上面这张图表示了两种模式的执行顺序区别,也是最根本的区别。

chatGPT 给出的定义如下:

异步任务是指在程序执行过程中,不会阻塞后续代码执行的任务。它们允许在后台执行,以便在等待长时间操作(例如网络请求、文件读写或计算密集型任务)完成时,程序能够继续执行其他任务,提高了程序的响应性和效率。 一个异步函数可以拆分为两个部分,一个是耗时部分,这部分时间内执行后面的代码来防止阻塞;另一个是执行回调任务部分,当耗时部分产生结果后,执行回调逻辑。

注:回调函数的定义是执行回调任务的一种形式,javascript 另外两种回调形式为 Promise,async/await。

setTimeout 函数

几乎所有讲解 js 异步机制的都会用 setTimeout 函数来举例子。它是一个最简单的执行异步任务的函数,意思是延迟一定时间后,执行一个回调函数。注意它有三个参数。

setTimeout(function, delay, [param1, param2, ...]);
  • function: 要执行的函数。
  • delay: 延迟的时间,单位为毫秒。
  • [param1, param2, ...]: 可选参数,这些参数会作为参数传递给执行的函数。

回调函数为什么被鄙视

解释完了同步和异步的差异以及异步的优势,下面说一下这篇文章要讨论的核心:假如目的是多个异步任务按顺序执行,应该如何书写易懂的代码?

上面说了异步任务的耗时部分结束后,要执行回调部分;如果用了回调函数的形式,且回调函数的执行结果要作为下一个异步任务的输入,那么就会有多个缩进,使得代码难以读懂,比如:

getUserInfo(function (userInfo) {
getUserOrders(userInfo.id, function (orders) {
orders.forEach(function (order) {
getOrderDetails(order.id, function (details) {
console.log("Order ID: " + order.id);
console.log("Order Details: " + details);
});
});
});
});

假设我们要依次执行三个异步操作:获取用户信息、获取用户订单列表,然后获取每个订单的详细信息。在上面的代码中,每个异步操作都依赖前一个操作的结果,因此它们被嵌套在一起。这会使代码变得难以理解。面对这样的嵌套回调,处理错误也会变得非常困难:你必须在“金字塔”的每一级处理错误,而不是在最高一级一次完成错误处理。

所以,现在的目标是:

  1. 减少嵌套,使得代码方便阅读和调试
  2. 在最高一级一次性处理错误

一句话总结:异步任务是让代码不会阻塞,而 promise 和 async/await 都是为了让多个异步任务按顺序执行的代码更易懂。

接下来看看 promise 和 async/await 是怎么让异步代码更易懂的?

promise(期约)

必须要说明的是,promise 的内容比较多,而且绕。不过,我们至少要记住它的表达形式:

function asyncTask(shouldResolve) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldResolve) {
resolve("Task completed successfully.");
} else {
reject(new Error("Task failed."));
}
}, 1000);
});
}

asyncTask()
.then((result) => {
console.log(result); // 输出: Task result
})
.catch((error) => {
console.error(error);
});

一个 Promise 实例被创建,并传入一个函数作为初始化参数。这个函数有两个参数,分别是 resolve 和 reject。resolve 含义是异步任务顺利执行,并返回一个值,这个值作为 resolve 的参数而调用。然后,then 方法里面传入的函数,其中的参数正是上面 resolve 种传入的参数。在这个例子里,"Task completed successfully."就是下面 then 里面的 result。另外,reject 指的是执行异步任务的过程中,发生了错误,这个错误经由 reject 函数,被下面的 catch 捕获。reject 抛出的错误只能被.catch 捕获,而不会被 try/catch 捕获。

我的理解是,resolve/reject 都是一种占位符。作用是当函数执行到这里,遇到他们俩,知道该往哪个方向走,并携带上参数。现在我们看看那两个目标是不是实现了。

  1. 减少嵌套:如果有进一步的的异步任务,可以放在 then 里面执行,有几层,就写几个 then,避免了嵌套。
  2. 一次性处理错误:最下面的.catch 会捕获.then 链中任意一步发生的错误(reject)。

async/await

同样的,先给出形式。

function asyncTask() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("Async task completed");
resolve("Task result");
}, 1000);
});
}

async function runTask() {
try {
const result = await asyncTask();
console.log(result); // 输出: Task result
} catch (error) {
console.error(error);
}
}

runTask();

在被 async 描述的函数体内,await 行下面的代码要在 resolve 后才能执行。这不是什么魔法,而是因为 await 是Promise.resolve().then()的语法糖。await 行下面的代码相当于是被包进了Promise.resolve().then()的 then 里面。所以上面的例子和下面等效:

function asyncTask() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("Async task completed");
resolve("Task result");
}, 1000);
});
}

async function runTask() {
asyncTask()
.then((result) => {
console.log(result); // 输出: Task result
})
.catch((error) => {
console.error(error);
});
}

runTask();

可以看出,换了一种写法后就和上小节的例子一模一样了。runTask 函数的 async 关键字虽然保留,但是如果内部没有 await 配合,也是没有任何作用的。

这个例子中,asyncTask 函数显式返回了一个 Promise 对象,实际上,这个函数可以返回非 Promise 对象,只是会被 await 隐式地转化为 Promise 对象。比如await 1

TypeScript 为 JavaScript 带来了什么

· 阅读需 6 分钟
Jason Lee
The owner of this blog site

TypeError 举例,TS 是如何解决的

当一项新技术摆在我这样的懒人前,有必要弄清学它的必要性,否则一定会半途而废。JavaScript 的过于灵活,一直以来为人诟病。它所带来的麻烦,主要体现在很多 TypeError 报错,直到运行时才会暴露出来。这一点在我的编程体验中最为深刻。以下是 chatgpt 对这个问题的理解和 typescript 能在多大程度上解决这类错误。

You:

JavaScript 中的 TypeError

ChatGPT:

在 JavaScript 中,TypeError 是一种常见的错误类型,当尝试对一个数据类型执行不适合的操作时会抛出。下面是一些常见的 TypeError 发生的情况:

  1. 访问未定义的属性
let obj;
console.log(obj.property); // TypeError: Cannot read property 'property' of undefined
  1. 调用非函数类型的变量
let nonFunction = 123;
nonFunction(); // TypeError: nonFunction is not a function
  1. 将值赋给只读属性
const obj = {};
Object.defineProperty(obj, "prop", {
value: 42,
writable: false,
});
obj.prop = 77; // TypeError: Cannot assign to read only property 'prop' of object
  1. 在基础数据类型上调用方法
null.someMethod(); // TypeError: Cannot read property 'someMethod' of null
  1. 使用new关键字调用不是构造函数的对象
let notAConstructor = {};
new notAConstructor(); // TypeError: notAConstructor is not a constructor

为了处理这些错误,可以使用 try...catch 语句来捕获和处理异常。例如:

try {
let obj;
console.log(obj.property);
} catch (e) {
if (e instanceof TypeError) {
console.error("TypeError caught:", e.message);
} else {
throw e;
}
}

通过合理的错误处理,可以使代码更加健壮,并能够更好地调试和维护。

You:

这些错误能通过把 js 改写为 typescript 来解决吗

ChatGPT:

将 JavaScript 转换为 TypeScript 可以预防许多类型相关的错误,因为 TypeScript 提供了静态类型检查和更严格的语法规则。然而,TypeScript 并不能完全消除所有运行时错误。让我们看看上述几种 TypeError 是如何在 TypeScript 中得到改进或预防的:

  1. 访问未定义的属性
let obj: { property?: string };
console.log(obj.property); // TypeScript 提示 obj 可能为 undefined

TypeScript 会警告你 obj 可能是 undefined,需要检查或使用类型保护。

  1. 调用非函数类型的变量
let nonFunction: any = 123;
(nonFunction as Function)(); // TypeScript 会警告类型不匹配

TypeScript 类型系统会阻止非函数类型的变量被调用。

  1. 将值赋给只读属性
interface MyObject {
readonly prop: number;
}
const obj: MyObject = { prop: 42 };
obj.prop = 77; // TypeScript 编译错误: Cannot assign to 'prop' because it is a read-only property.

TypeScript 通过 readonly 修饰符强制只读属性不可被重新赋值。

  1. 在基础数据类型上调用方法
let nullable: null | { someMethod: () => void } = null;
nullable.someMethod(); // TypeScript 提示 nullable 可能为 null

TypeScript 会提示 nullable 可能是 null,需要进行检查。

  1. 使用new关键字调用不是构造函数的对象
let notAConstructor: any = {};
new notAConstructor(); // TypeScript 编译错误: Cannot use 'new' with an expression whose type lacks a call or construct signature.

TypeScript 会阻止在非构造函数的对象上使用 new 关键字。

TypeScript 通过静态类型检查、接口、类型断言等机制,大大减少了因为类型错误而导致的运行时异常。尽管如此,TypeScript 不能完全避免所有错误,特别是那些涉及到动态数据和外部输入的错误,因此在编写 TypeScript 代码时仍需谨慎,并结合适当的错误处理机制。

TS 很强,但是还不够

TypeScript 类型检查的范围只局限在代码编译阶段,运行时是无能为力的。比如一次数据库查询期望返回一个数组格式的数据,但是因为某种原因变成了 undefined,那么后续代码用到这个数组的地方就会报错了。所以,涉及到动态数据的,应该要手动加类型检查,比如说

if (!Array.isArray(o)) {
throw new Error();
}

强类型?弱类型?

ts 是在 js 的基础上加上了类型,因此成了一门静态语言。但是,这种静态类型还要加一个"弱"的形容词。这是因为,ts 和 js 一样会执行隐式转换而不会报错。

console.log(1 + "1");
// 打印出字符串 '11'

上面这段代码,js/ts 都能执行成功。

nestjs入门

· 阅读需 11 分钟
Jason Lee
The owner of this blog site

前言

软开团队之前做过的一个项目,用 nestjs 做的。我听到时很惊讶,部门负责人竟然这么敢用当时看起来还那么稚嫩的 nodejs 作为后端语言。让我们开始吧。

安装

类似于 vue2,也要安装一个脚手架。启动后,访问 localhost:3000,出现一个朴实无华的 Hello world,甚至没有用标题 html 标签:

16907906119941690790611215.png

nestjs 和 express 的关系

NestJS 和 Express 之间有密切的关系,可以将 NestJS 视为建立在 Express 之上的框架。NestJS 的 HTTP 模块和功能实际上是建立在 Express 的基础之上的,并且可以与 Express 的功能无缝交互。

具体来说,NestJS 在底层使用 Express 来处理 HTTP 请求和构建 Web 应用程序。这意味着,NestJS 的 HTTP 模块实际上是使用 Express 的路由和中间件系统来定义路由和处理请求。因此,你可以在 NestJS 应用程序中直接使用 Express 中的中间件,也可以使用 NestJS 提供的中间件。

虽然 NestJS 在底层使用了 Express,但它为开发者提供了一种更现代、模块化的方法来构建应用程序。NestJS 引入了一系列新的概念,如模块、控制器、提供者、依赖注入等,这些使得代码更易于组织、测试和维护。此外,NestJS 还支持多种其他功能,如 WebSocket、GraphQL 等,可以让你构建更强大的应用程序。

总的来说,NestJS 是建立在 Express 之上的,它保留了 Express 的灵活性和强大性能,同时为开发者提供了更高级的抽象和功能,使得构建复杂的服务器端应用程序更加容易和愉快。

项目结构

我是跟着油管的教程敲了一个基础小项目。下面是项目目录的截图。

16909392755291690939274607.png

nestjs 推荐通过脚手架工具来生成代码文件,这样的初始代码来自模板文件。所有可自动生成的文件如下:

16909397445231690939744129.png

这里和我熟悉的 django 框架做一个对比。

nestjsdjangonest-cli说明
moduleappnest generate module users模块
controllersurls.pynest generate controller /users/controllers/users路由
servicesviews.pynest g s /users/services/users视图,运行逻辑代码
pipesforms.pynest g pipe /users/pipes/ValidateCreateUser对表单或者传来的数据进行验证
middlewaremiddlewarenest g mi /users/middlewares/example中间件
guard权限装饰器nest g guard /users/guards/Auth权限控制

其它的,app.module.ts对模块进行注册,相当于 django 中 settings.py 中的 INSTALLED_APPS。

DTO(Data Transfer Object)

在 NestJS 中,DTO(Data Transfer Object)是一种用于数据传输的对象。DTO 主要用于在应用程序的不同层之间传输数据,通常在控制器(Controller)层和服务(Service)层之间进行数据传递。DTO 的目的是将数据从一个地方传输到另一个地方,并在传输过程中进行数据验证和转换。

DTO 在应用程序中扮演了重要的角色,特别是在处理 HTTP 请求和响应的过程中。它们可以用于将请求数据从控制器传递给服务,也可以用于将服务返回的数据转换为适合返回给客户端的格式。

通常,DTO 对象会在不同层之间定义,并在控制器和服务中使用。DTO 可以用于以下几个方面:

请求数据验证:在控制器中,DTO 可以用于验证客户端传递的请求数据是否符合预期的格式和规则。通过使用装饰器来定义 DTO 类的属性,NestJS 可以自动验证传入的数据,并根据定义的规则返回验证错误。

数据传输:在服务层和控制器之间传递数据。服务层可以接收 DTO 对象,执行相应的业务逻辑,然后返回 DTO 对象,控制器再将其转换为 HTTP 响应返回给客户端。

数据转换:DTO 可以用于将服务返回的数据转换为适合返回给客户端的格式。例如,将数据库查询的结果转换为 JSON 格式返回给客户端。

隐藏敏感信息:在某些情况下,DTO 可以用于隐藏敏感信息,确保客户端只能获得必要的数据而不会暴露不应该看到的信息。

在 NestJS 中,你可以使用 @nestjs/swagger 模块来自动生成 API 文档,并自动将 DTO 类中的装饰器用于验证请求数据。

下面是一个简单的例子,展示了在 NestJS 中定义一个 DTO 类的示例:

import { IsString, IsInt } from 'class-validator';

export class CreateUserDto {
@IsString()
readonly name: string;

@IsInt()
readonly age: number;

@IsString()
readonly email: string;
}

在上面的例子中,我们定义了一个 CreateUserDto 类,该类用于在控制器中接收创建用户的请求数据。使用 @IsString() 和 @IsInt() 装饰器来定义每个属性的验证规则。在控制器中,我们可以使用该 DTO 类来验证客户端传入的数据是否符合预期的规则。

两处类型验证的区别

由于请求传入参数和保存在数据库的参数可能不一致,所以需要两种验证。请求参数的验证时 放在 dtos 文件夹下,在 controller 中使用。而保存数据库的验证放在 utils 文件夹,在 service 层使用。

依赖注入

GPT:

依赖注入是一种编程的方式,用于管理代码中不同部分之间的依赖关系。在软件开发中,一个模块可能会需要使用另一个模块提供的功能或数据,而依赖注入就是一种让这种关系更加松散和可维护的方法。

举个例子,假设你在开发一个网站后端,使用了 NestJS 这个框架。你需要创建一些控制器来处理不同的请求,同时还需要连接数据库来存储和检索数据。这时就涉及到依赖关系:控制器依赖于数据库连接来获取数据。

在使用依赖注入之前,你可能会在每个控制器内部创建数据库连接,但这会导致代码重复和难以维护。而使用依赖注入,你可以这样做:

定义一个数据库连接的服务(或者称之为提供者):在 NestJS 中,你可以创建一个类,用于创建和管理数据库连接。

在控制器中声明依赖:在你的控制器类中,你可以声明需要使用的依赖,比如数据库连接。

在模块中设置依赖关系:在 NestJS 中,你需要将控制器和提供者(数据库连接)放入同一个模块中,并在模块的配置中告诉 NestJS 如何建立这些依赖关系。

NestJS 完成注入:当你启动应用程序时,NestJS 将根据你在模块中的配置,自动将数据库连接注入到控制器中。这意味着你不必自己管理控制器和连接之间的关系,NestJS 会帮你处理。

通过依赖注入,你可以更好地组织代码,降低模块之间的耦合度,使得代码更容易测试和维护。

下面是一个伪代码示例,演示如何在 NestJS 中使用依赖注入:

// 数据库连接提供者
@Injectable()
class DatabaseService {
connect() {
// 连接数据库的逻辑
}
}

// 控制器
@Controller()
class UserController {
constructor(private databaseService: DatabaseService) {}

getUser(id: number) {
const user = this.databaseService.fetchUser(id);
return user;
}
}

// 模块配置
@Module({
providers: [DatabaseService],
controllers: [UserController],
})
class UserModule {}

在这个示例中,DatabaseService 负责数据库连接,UserController 控制器依赖于 DatabaseService 来获取用户信息。在 UserModule 模块中,我们通过将 DatabaseService 提供者添加到 providers 列表中,将 UserController 控制器添加到 controllers 列表中,NestJS 将自动完成依赖注入。

简单来说,nestjs 的依赖注入,就类似于 django 中的 urls 和 views 相分离。

LINUX 基础操作(基于UBUNTU)

· 阅读需 8 分钟
Jason Lee
The owner of this blog site

前言

跟随全栈学习路线之 linux 系统熟悉。由于 linux 的命令很多,且有些命令很长,很难记住,但是借助 gpt,无需强调记住命令,而是重点学习 linux 系统的工作原理,以及和 win 系统不同的操作逻辑。

如何定义“常见”的命令呢?我认为,看到那些习以为常的 windows 操作在 linux 中的实现,就可以认为是常见命令加以记忆。

directory

删除某一个文件夹时,如果文件夹下有文件,是不能直接删除的,得用rm -r <directory>. -r 表示递归的删除,即将所有存在的文件全部删除。

文件操作

因为 linux 内万物皆文件,所以下面的命令适用于 linux 内任何实体。

复制粘贴/移动

复制粘贴文件时,并不是像 win 下有分为 copy&paste 两个动作,而是一个命令完成:cp <src> <target>. 如果要将整个文件夹下的文件复制到另一个地方,要分两种情况,要么是所有文件的转移:cp -r . <target> 或者以文件夹的形式复制:cp -r <dirname> <target>.

mv 命令从字面上是移动(剪切粘贴),也可作为重命名文件的命令使用

linux 目录结构

win 的所有文件都存在 C 盘(假如不分区),而 linux 的根目录则是朴实无华的/,类似于 C 盘。用 xshell 等工具登录后的着陆目录可能是你的用户文件夹,比如/root

文件的增删改查

创建空文件用touch,但是好像没什么必要。绝大部分场景是创建文本文件,最好是vi

  1. 删:rm

  2. 改:vi

查有两个层面:一是查看文件属性,用ls -lh filename。二是查看文件内容。小文件直接用 vi,大文件(一般是日志),要么查看尾部几条tail -n 200 filename,要么用less进入文件,熟练运用space/b进行上下翻页。

vi 的详细命令本文后面会说。

system info

最主要的三个状态:cpu,内存和磁盘。

cpu 和内存

可以通过top或者htop(需要另外安装)

top 的输出:

htop 的输出:

htop 的信息更清晰一些。在进入 top 界面后,可以输入下列快捷键改变实时显示:

  • q:退出 top

  • P:按 CPU 使用率排序(默认)。

  • M:按内存使用率排序。

  • k:结束一个进程(输入进程 ID)。

磁盘

磁盘大概不必查看实时状态。命令为df -h.

用centos的截图代替了

package manager

windows 下所有的软件安装都是走可视化窗口,而 ubuntu 一般都是通过命令行。搜索软件:sudo apt search <name>, 安装:sudo apt install <name>, 卸载:sudo apt remove <name>

或者,也有的软件是通过下载压缩包,并解压后得到 deb 后缀的安装包,运行sudo dpkg -i <name>.

注意:软件安装都需要 sudo 的权限,因为安装过程都是对系统文件的修改。

text editor

值得说明的是,用 vscode 连接远程的,可以直接用图形页面编辑文本,也是优先选择。可选工具:vi, vim, nano 等。

~~nano 在我心中取代了 vim,因为它的操作逻辑更简单直观。~~Nano 不是自带的,使用还得安装;而大部分系统(ubuntu/centos)都自带 vi。所以,还是掌握 vi 为好。

vi 常用命令

  • 搜索文本

    • /pattern:向下搜索 pattern

    • ?pattern:向上搜索 pattern

    • n:重复上一次搜索(相同方向)。

    • N:反向重复上一次搜索。

  • 显示/隐藏行号: :set nu/set nonu

  • 跳转到某一行: :n

  • 翻页Ctrl + f向下翻页;Ctrl + b向上翻页;G移动到文件末尾;gg移动到文件开头。

打包压缩

Linux 的打包和压缩实际上是两个独立的过程,不同于 windows 的默认集中在一起完成。

  • 打包(Archive) : 仅将多个文件合并成一个文件,并不压缩大小,常用 tar 命令。

  • 压缩(Compression) : 通过算法减少文件大小,常用 gzipbzip2xz 命令。

  • 打包和压缩结合使用 通常情况下,打包和压缩结合使用可以将多个文件合并为一个文件,同时减少其总大小。在 Linux 中,最常用的组合是 tar 命令和 gzipbzip2 压缩工具的组合。

  • 打包并压缩 :

tar -cvzf archive.tar.gz file1.txt file2.txt  # 打包并使用 gzip 压缩
tar -cvjf archive.tar.bz2 file1.txt file2.txt # 打包并使用 bzip2 压缩
tar -cvJf archive.tar.xz file1.txt file2.txt # 打包并使用 xz 压缩
  • 解压并解包 :
tar -xvzf archive.tar.gz  # 解压并解包 gzip 压缩的 tar 文件
tar -xvjf archive.tar.bz2 # 解压并解包 bzip2 压缩的 tar 文件
tar -xvJf archive.tar.xz # 解压并解包 xz 压缩的 tar 文件

选项解释:

  • -c:创建新的打包文件(create)。

  • -v:显示打包过程的详细信息(verbose)。

  • -f:指定输出文件名(file)。

  • -t:查看打包文件的内容(list)。

  • -x:解包(extract)。

  • -z:使用 gzip 压缩。

  • -j:使用 bzip2 压缩。

  • -J:使用 xz 压缩。

软硬连接对标 windows 的快捷方式。区别如下:

比较项软连接(Symbolic Link)硬连接(Hard Link)
创建命令ln -s <目标> <链接名> ln <目标> <链接名>
是否可以跨文件系统
是否可以指向目录否(通常不能指向目录)
是否依赖目标路径是(依赖目标路径)否(与目标文件共享相同的 inode)
目标删除的影响目标被删除,链接失效(断链)目标被删除,硬链接仍可访问文件内容
是否共享 inode否(独立的 inode)是(共享相同的 inode)
用途用于创建目录快捷方式或跨分区引用用于在同一分区内创建文件的多个引用

快速部署深度学习模型:gradio初体验

· 阅读需 6 分钟
Jason Lee
The owner of this blog site

前言

需求:将机器学习的模型变成网页应用,并部署到公网上用于访问。

前几个月,当时也有类似的需求,想将自己毕业论文中的一个图像处理模型部署到公网。当时考虑的是,用 django 或者 flask 结合 pytorch 在本地搭一个应用,实现图片进出预训练模型的逻辑,然后用内网穿透的工具,使得外部可以访问到我的主机。但是这就带来许多问题,比如当时找了好几个内网穿透工具,要么收费,要么不好用。而且,主机总会有关机的时候,那么关机后服务也就停止了。一时间迟迟无法入手,当时就暂时放弃了。前阵子听说了 gradio 这个好东西,一个 python 包,通过几行代码,就可以在本地起一个应用,不仅带有简洁的 UI,而且内置了内网穿透。当时觉得很困难的问题,竟然一下迎刃而解了。感叹现在深度学习社区发展的速度。

gradio 本地运行

下面以知乎上的 rgb2gray 为例,讲解 gradio 库的使用。新建一个虚拟环境,安装上 gradio 包:

pip install -Uq gradio ## -U表示安装最新版本的包,-q表示静默安装,避免命令行污染

新建一个 app.py:

import gradio as gr
import cv2

def to_black(image):
output = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
return output

interface = gr.Interface(fn=to_black, inputs="image", outputs="image")
interface.launch(share=True)

运行 py 文件,gradio 在本地启动一个 HTTP 服务器,并通过内网穿透将该服务器暴露给公共互联网。局域网和公网链接都会输出在命令行终端。但是生成的公网链接会在 72 小时后过期,因此它是一个临时链接。如果要实现永久访问,需要将代码以及预训练模型(如有)上传到 huggingface space 上。

huggingface space

huggingface space 是一个类似 github 的地方,可以用来存 py 文件和预训练模型,并且在和 gradio 绑定后,可以实现免费的部署。如果是新电脑,运行gradio deploy,按指示添加 write token,绑定 huggingface 账号。也可以通过 git clone 命令,下载空间到本地再修改。新建 space 时的信息:

16886436128041688643612164.png

除了一个基础的 cpu 是免费的,其他都是按小时收费的。说实话还是蛮贵的,比如 T4 gpu,colab 和 kaggle 都是免费提供的,这里收 0.6 美刀/小时。当代码每次有更改时,空间会自动刷新并运行一次,可通过点击红框内的按钮查看进度:

16886441928151688644192174.png

成功之后,可以通过分享这个 space 的链接,来实现在线演示。

ps: 如何找到自己创建过的 space。要通过点击头像,进入个人主页。

访问速度对比

这里做一个简单的测试,本例的 rgb2gray 是一个非常简单的函数,cpu 都能瞬间完成,下面分别测试 gradio 在代码存在本地时通过 localhost 和内网穿透以及代码在 hf space 下的处理速度。图片尺寸为 1280*853,浏览器为 firefox。

方式时间(估计)使用场景
localhost0.4s本地演示给自己或者旁边的人看
内网穿透24s有外部访问的需求,且本地有 gpu,能覆盖网速带来的负面影响
hf space4s有外部访问或者长时间保持服务的需求,可以购买 gpu 时长

可以看出,内网穿透最慢,localhost 最快。也可以理解,内网穿透的网络传输方式最复杂,受网速波动影响大。

api

除了 gradio 提供的 UI,也可以自己设计 UI,并利用 gradio 提供的 api 接口,实现模型推理。但是,当我按照文档的代码运行时,遇到了报错:

ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。

现在暂时未解决。

结语

记录了 gradio 的初级用法,并给出了不同场景下的使用方案。有了 gradio,深度学习模型部署变得省事多了。