{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":20270,"databundleVersionId":1222630,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":1873742,"sourceType":"datasetVersion","datasetId":1115384}],"dockerImageVersionId":30919,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install numpy pandas Pillow torch git+https://github.com/openai/CLIP.git pydicom tqdm scikit-learn imageio opencv-python scipy\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:32:08.000483Z","iopub.execute_input":"2025-05-07T21:32:08.000717Z","iopub.status.idle":"2025-05-07T21:32:16.576439Z","shell.execute_reply.started":"2025-05-07T21:32:08.000685Z","shell.execute_reply":"2025-05-07T21:32:16.575368Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from scipy.spatial import distance\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport torch\nimport clip\nimport pydicom\nfrom tqdm import tqdm\nfrom sklearn.linear_model import LogisticRegression, LinearRegression\nimport os\nimport imageio\nimport cv2\n\n\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        os.path.join(dirname, filename)\nprint(\"Path Join Completed\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:32:16.577878Z","iopub.execute_input":"2025-05-07T21:32:16.578200Z","iopub.status.idle":"2025-05-07T21:34:11.541365Z","shell.execute_reply.started":"2025-05-07T21:32:16.578166Z","shell.execute_reply":"2025-05-07T21:34:11.540575Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_meta= pd.read_csv('/kaggle/input/cbis-ddsm-breast-cancer-image-dataset/csv/meta.csv')\ndf_meta.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:11.543128Z","iopub.execute_input":"2025-05-07T21:34:11.543620Z","iopub.status.idle":"2025-05-07T21:34:11.605416Z","shell.execute_reply.started":"2025-05-07T21:34:11.543596Z","shell.execute_reply":"2025-05-07T21:34:11.604691Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_dicom = pd.read_csv('/kaggle/input/cbis-ddsm-breast-cancer-image-dataset/csv/dicom_info.csv')\ndf_dicom.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:11.606529Z","iopub.execute_input":"2025-05-07T21:34:11.606806Z","iopub.status.idle":"2025-05-07T21:34:11.796118Z","shell.execute_reply.started":"2025-05-07T21:34:11.606785Z","shell.execute_reply":"2025-05-07T21:34:11.795286Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_dicom.SeriesDescription.unique()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:11.797049Z","iopub.execute_input":"2025-05-07T21:34:11.797372Z","iopub.status.idle":"2025-05-07T21:34:11.803808Z","shell.execute_reply.started":"2025-05-07T21:34:11.797317Z","shell.execute_reply":"2025-05-07T21:34:11.803026Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cropped_images = df_dicom[df_dicom.SeriesDescription=='cropped images'].image_path\ncropped_images.head(5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:11.804645Z","iopub.execute_input":"2025-05-07T21:34:11.804943Z","iopub.status.idle":"2025-05-07T21:34:11.823866Z","shell.execute_reply.started":"2025-05-07T21:34:11.804915Z","shell.execute_reply":"2025-05-07T21:34:11.823087Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"full_mammo = df_dicom[df_dicom.SeriesDescription=='full mammogram images'].image_path\nfull_mammo.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:11.824676Z","iopub.execute_input":"2025-05-07T21:34:11.824969Z","iopub.status.idle":"2025-05-07T21:34:11.840486Z","shell.execute_reply.started":"2025-05-07T21:34:11.824938Z","shell.execute_reply":"2025-05-07T21:34:11.839556Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"roi_img = df_dicom[df_dicom.SeriesDescription=='ROI mask images'].image_path\nroi_img.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:11.841165Z","iopub.execute_input":"2025-05-07T21:34:11.841407Z","iopub.status.idle":"2025-05-07T21:34:11.856021Z","shell.execute_reply.started":"2025-05-07T21:34:11.841370Z","shell.execute_reply":"2025-05-07T21:34:11.855326Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Check if row 8027 exists in roi_img\nif 8027 in roi_img.index:\n    print(\"Row 8027 exists in roi_img.\")\nelse:\n    print(\"Row 8027 does not exist in roi_img.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:11.858486Z","iopub.execute_input":"2025-05-07T21:34:11.858675Z","iopub.status.idle":"2025-05-07T21:34:11.870498Z","shell.execute_reply.started":"2025-05-07T21:34:11.858660Z","shell.execute_reply":"2025-05-07T21:34:11.869743Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"imdir = '../input/cbis-ddsm-breast-cancer-image-dataset/jpeg'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:11.872024Z","iopub.execute_input":"2025-05-07T21:34:11.872298Z","iopub.status.idle":"2025-05-07T21:34:11.886827Z","shell.execute_reply.started":"2025-05-07T21:34:11.872271Z","shell.execute_reply":"2025-05-07T21:34:11.886169Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cropped_images = cropped_images.replace('CBIS-DDSM/jpeg', imdir, regex=True)\nfull_mammo = full_mammo.replace('CBIS-DDSM/jpeg', imdir, regex=True)\nroi_img = roi_img.replace('CBIS-DDSM/jpeg', imdir, regex=True)\n\n# view new paths\nprint('Cropped Images paths:\\n')\nprint(cropped_images.iloc[0])\nprint('Full mammo Images paths:\\n')\nprint(full_mammo.iloc[0])\nprint('ROI Mask Images paths:\\n')\nprint(roi_img.iloc[0])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:11.887528Z","iopub.execute_input":"2025-05-07T21:34:11.887794Z","iopub.status.idle":"2025-05-07T21:34:11.911721Z","shell.execute_reply.started":"2025-05-07T21:34:11.887767Z","shell.execute_reply":"2025-05-07T21:34:11.910986Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"full_mammo_dict = dict()\ncropped_images_dict = dict()\nroi_img_dict = dict()\n\nfor dicom in full_mammo:\n    key = dicom.split(\"/\")[4]\n    full_mammo_dict[key] = dicom\nfor dicom in cropped_images:\n    key = dicom.split(\"/\")[4]\n    cropped_images_dict[key] = dicom\nfor dicom in roi_img:\n    key = dicom.split(\"/\")[4]\n    roi_img_dict[key] = dicom\n\n# view keys\nnext(iter((full_mammo_dict.items())))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:11.912402Z","iopub.execute_input":"2025-05-07T21:34:11.912582Z","iopub.status.idle":"2025-05-07T21:34:11.935821Z","shell.execute_reply.started":"2025-05-07T21:34:11.912566Z","shell.execute_reply":"2025-05-07T21:34:11.935037Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"mass_train = pd.read_csv('/kaggle/input/cbis-ddsm-breast-cancer-image-dataset/csv/mass_case_description_train_set.csv')\nmass_test = pd.read_csv('/kaggle/input/cbis-ddsm-breast-cancer-image-dataset/csv/mass_case_description_test_set.csv')\n\nmass_train.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:11.936715Z","iopub.execute_input":"2025-05-07T21:34:11.936980Z","iopub.status.idle":"2025-05-07T21:34:11.995997Z","shell.execute_reply.started":"2025-05-07T21:34:11.936961Z","shell.execute_reply":"2025-05-07T21:34:11.995113Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def fix_image_path(data):\n    \"\"\"correct dicom paths to correct image paths\"\"\"\n    for index, img in enumerate(data.values):\n        img_name = img[11].split(\"/\")[2]\n        data.iloc[index,11] = full_mammo_dict[img_name]\n        img_name = img[12].split(\"/\")[2]\n        data.iloc[index,12] = cropped_images_dict[img_name]\n        img_name = img[13].split(\"/\")[2]\n        data.iloc[index,13] = roi_img_dict[img_name]\n        \n# apply to datasets\nfix_image_path(mass_train)\nfix_image_path(mass_test)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:11.996894Z","iopub.execute_input":"2025-05-07T21:34:11.997169Z","iopub.status.idle":"2025-05-07T21:34:12.697310Z","shell.execute_reply.started":"2025-05-07T21:34:11.997149Z","shell.execute_reply":"2025-05-07T21:34:12.696406Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"mass_train = mass_train.rename(columns={'left or right breast': 'left_or_right_breast',\n                                           'image view': 'image_view',\n                                           'abnormality id': 'abnormality_id',\n                                           'abnormality type': 'abnormality_type',\n                                           'mass shape': 'mass_shape',\n                                           'mass margins': 'mass_margins',\n                                           'image file path': 'image_file_path',\n                                           'cropped image file path': 'cropped_image_file_path',\n                                           'ROI mask file path': 'ROI_mask_file_path'})\n\nmass_train.head(5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:12.698257Z","iopub.execute_input":"2025-05-07T21:34:12.698540Z","iopub.status.idle":"2025-05-07T21:34:12.711553Z","shell.execute_reply.started":"2025-05-07T21:34:12.698511Z","shell.execute_reply":"2025-05-07T21:34:12.710787Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"mass_train['mass_shape'] = mass_train['mass_shape'].bfill()\nmass_train['mass_margins'] = mass_train['mass_margins'].bfill()\n\n#check null values\nmass_train.isnull().sum()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:12.712340Z","iopub.execute_input":"2025-05-07T21:34:12.712566Z","iopub.status.idle":"2025-05-07T21:34:12.734006Z","shell.execute_reply.started":"2025-05-07T21:34:12.712548Z","shell.execute_reply":"2025-05-07T21:34:12.733367Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f'Shape of mass_train: {mass_train.shape}')\nprint(f'Shape of mass_test: {mass_test.shape}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:12.734829Z","iopub.execute_input":"2025-05-07T21:34:12.735094Z","iopub.status.idle":"2025-05-07T21:34:12.749121Z","shell.execute_reply.started":"2025-05-07T21:34:12.735059Z","shell.execute_reply":"2025-05-07T21:34:12.748396Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"mass_test = mass_test.rename(columns={'left or right breast': 'left_or_right_breast',\n                                           'image view': 'image_view',\n                                           'abnormality id': 'abnormality_id',\n                                           'abnormality type': 'abnormality_type',\n                                           'mass shape': 'mass_shape',\n                                           'mass margins': 'mass_margins',\n                                           'image file path': 'image_file_path',\n                                           'cropped image file path': 'cropped_image_file_path',\n                                           'ROI mask file path': 'ROI_mask_file_path'})","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:12.750011Z","iopub.execute_input":"2025-05-07T21:34:12.750302Z","iopub.status.idle":"2025-05-07T21:34:12.764146Z","shell.execute_reply.started":"2025-05-07T21:34:12.750275Z","shell.execute_reply":"2025-05-07T21:34:12.763412Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"mass_test['mass_margins'] = mass_test['mass_margins'].bfill()\n\n#check null values\nmass_test.isnull().sum()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:12.764944Z","iopub.execute_input":"2025-05-07T21:34:12.765228Z","iopub.status.idle":"2025-05-07T21:34:12.781817Z","shell.execute_reply.started":"2025-05-07T21:34:12.765201Z","shell.execute_reply":"2025-05-07T21:34:12.781162Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport matplotlib.image as mpimg\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:12.782536Z","iopub.execute_input":"2025-05-07T21:34:12.782813Z","iopub.status.idle":"2025-05-07T21:34:12.794967Z","shell.execute_reply.started":"2025-05-07T21:34:12.782787Z","shell.execute_reply":"2025-05-07T21:34:12.794202Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(mass_train['cropped_image_file_path'].head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:12.795631Z","iopub.execute_input":"2025-05-07T21:34:12.795828Z","iopub.status.idle":"2025-05-07T21:34:12.808663Z","shell.execute_reply.started":"2025-05-07T21:34:12.795811Z","shell.execute_reply":"2025-05-07T21:34:12.807991Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(mass_train['ROI_mask_file_path'].head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:12.809450Z","iopub.execute_input":"2025-05-07T21:34:12.809696Z","iopub.status.idle":"2025-05-07T21:34:12.822509Z","shell.execute_reply.started":"2025-05-07T21:34:12.809677Z","shell.execute_reply":"2025-05-07T21:34:12.821781Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def display_images(column, number):\n    # create figure and axes\n    number_to_visualize = number\n    rows = 1\n    cols = number_to_visualize\n    fig, axes = plt.subplots(rows, cols, figsize=(15, 5))\n    \n    # Loop through rows and display images\n    for index, row in mass_train.head(number_to_visualize).iterrows():\n        image_path = row[column]\n        image = mpimg.imread(image_path)\n        ax = axes[index]\n        ax.imshow(image, cmap='gray')\n        ax.set_title(f\"{row['pathology']}\")\n        ax.axis('off')\n    plt.tight_layout()\n    plt.show()\n\nprint('Full Mammograms:\\n')\ndisplay_images('image_file_path', 5)\nprint('Cropped Mammograms:\\n')\ndisplay_images('cropped_image_file_path', 5)\nprint('ROI mask:\\n')\ndisplay_images('ROI_mask_file_path', 5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:12.823209Z","iopub.execute_input":"2025-05-07T21:34:12.823437Z","iopub.status.idle":"2025-05-07T21:34:22.988995Z","shell.execute_reply.started":"2025-05-07T21:34:12.823418Z","shell.execute_reply":"2025-05-07T21:34:22.988082Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_1122= pd.read_csv('/kaggle/input/siim-isic-melanoma-classification/train.csv')\nprint(df_1122.head(5))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:22.989946Z","iopub.execute_input":"2025-05-07T21:34:22.990249Z","iopub.status.idle":"2025-05-07T21:34:23.068653Z","shell.execute_reply.started":"2025-05-07T21:34:22.990214Z","shell.execute_reply":"2025-05-07T21:34:23.067640Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_2211 = pd.read_csv('/kaggle/input/siim-isic-melanoma-classification/test.csv')\nprint(df_2211.head(5))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:34:23.069421Z","iopub.execute_input":"2025-05-07T21:34:23.069661Z","iopub.status.idle":"2025-05-07T21:34:23.091906Z","shell.execute_reply.started":"2025-05-07T21:34:23.069641Z","shell.execute_reply":"2025-05-07T21:34:23.091172Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n\ndef process_dcm(file_path):\n    ds = pydicom.dcmread(file_path)\n    im = ds.pixel_array\n    im = im.astype(float)\n    # simple normalization, convert to RGB\n    im = im / im.max()\n    im2 = np.zeros(list(im.shape) + [3])\n    for i in range(3):\n        im2[:, :, i] = im\n    im = (255 * im2).astype(np.uint8)\n\n    return im\n\n\ndef create_clip_feature_mat(file_list, clip_model, preprocess_fxn):\n    X = np.zeros((len(file_list), 512)) # 512 is feature dimension\n    for i, f in tqdm(enumerate(file_list), total=len(file_list)):\n        if '.dcm' in f:\n            im = Image.fromarray(process_dcm(f))\n        else:\n            im = Image.open(f)\n            if im.mode != 'RGB':             \n                im = im.convert('RGB')    \n        im = preprocess_fxn(im).unsqueeze(0).to(device)\n        with torch.no_grad():\n            image_features = clip_model.encode_image(im)\n        X[i] = image_features[0].cpu()\n\n    return X\n\n\nfrom scipy.spatial import distance\nfrom sklearn.metrics import (\n    roc_auc_score, accuracy_score, f1_score, recall_score, confusion_matrix\n)\nfrom statsmodels.stats.proportion import proportion_confint\nimport pandas as pd\nimport numpy as np\nimport os\n\ndef fit_words(train_df, test_df, device, word_list, save_dir, save_tag):\n    # ——— 1) 加载 CLIP ———\n    clip_model, preprocess_fxn = clip.load(\"ViT-B/32\", device=device)\n\n    # ——— 2) 提取 CLIP image 特征 ———\n    X_train = create_clip_feature_mat(train_df.file_path.values, clip_model, preprocess_fxn)\n\n    # ——— 3) CLIP feature 上的 LogisticRegression ———\n    classifier = LogisticRegression(\n        random_state=0, C=1, max_iter=1000, verbose=1, fit_intercept=False\n    )\n    classifier.fit(X_train, train_df.label.values)\n\n    # ——— 4) 提取 word embeddings & 拟合 word→coef ———\n    tokened_words  = clip.tokenize(word_list).to(device)\n    with torch.no_grad():\n        word_features = clip_model.encode_text(tokened_words)  # [n_words, 512]\n\n    weights_model = LinearRegression(fit_intercept=False)\n    weights_model.fit(\n        word_features.cpu().T,            # [512, n_words]\n        classifier.coef_[0]               # [512, ]\n    )\n\n    # ——— 5) 保存并打印 word weights \n    word_df = pd.DataFrame({\n        'word':    word_list,\n        'weight':  weights_model.coef_\n    }).set_index('word').sort_values('weight')\n\n    # 计算每个词的绝对权重占比\n    abs_sum = word_df['weight'].abs().sum()\n    word_df['prop'] = word_df['weight'].abs() / abs_sum\n\n\n    word_df.to_csv(os.path.join(save_dir, f'word_weights-{save_tag}.csv'))\n    print(f\"\\n=== [{save_tag}] Descriptor Weights & Proportions ===\")\n    print(word_df.round(3))\n\n    # ——— 6) 测试集上 CLIP→LR 的性能 ———\n    X_test   = create_clip_feature_mat(test_df.file_path.values, clip_model, preprocess_fxn)\n    y_true   = test_df.label.values\n    y_score  = classifier.predict_proba(X_test)[:, 1]\n    y_pred   = classifier.predict(X_test)\n\n    # 6a) 基本指标\n    acc   = accuracy_score(y_true, y_pred)\n    auc   = roc_auc_score(y_true, y_score)\n    f1    = f1_score(y_true, y_pred)\n    sens  = recall_score(y_true, y_pred)\n    tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()\n    spec  = tn/(tn+fp)\n\n    # 6b) 置信区间\n    n = len(y_true)\n    ci_acc_low, ci_acc_hi   = proportion_confint(acc*n, n, method='wilson')\n    ci_sens_low, ci_sens_hi = proportion_confint(tp, tp+fn, method='wilson')\n    ci_spec_low, ci_spec_hi = proportion_confint(tn, tn+fp, method='wilson')\n\n    print(f\"\\n=== [{save_tag}] CLIP→LR  Performance ===\")\n    print(f\"Accuracy:    {acc:.3f} (95% CI [{ci_acc_low:.3f},{ci_acc_hi:.3f}])\")\n    print(f\"AUC:         {auc:.3f}\")\n    print(f\"F1:          {f1:.3f}\")\n    print(f\"Sensitivity: {sens:.3f} (95% CI [{ci_sens_low:.3f},{ci_sens_hi:.3f}])\")\n    print(f\"Specificity: {spec:.3f} (95% CI [{ci_spec_low:.3f},{ci_spec_hi:.3f}])\")\n\n    # ——— 7) Descriptor→LR Performance ———\n    W = word_features.cpu().numpy()                # [n_words,512]\n    X_desc_train = X_train.dot(W.T)                # [n_samples, n_words]\n    X_desc_test  = X_test.dot(W.T)\n\n    desc_clf = LogisticRegression(random_state=0, C=1, max_iter=1000, fit_intercept=False)\n    desc_clf.fit(X_desc_train, train_df.label.values)\n\n    y_score_d = desc_clf.predict_proba(X_desc_test)[:,1]\n    y_pred_d  = desc_clf.predict(X_desc_test)\n\n    acc_d   = accuracy_score(y_true, y_pred_d)\n    auc_d   = roc_auc_score(y_true, y_score_d)\n    f1_d    = f1_score(y_true, y_pred_d)\n    sens_d  = recall_score(y_true, y_pred_d)\n    tn, fp, fn, tp = confusion_matrix(y_true, y_pred_d).ravel()\n    spec_d  = tn/(tn+fp)\n\n    print(f\"\\n=== [{save_tag}] Descriptor→LR Performance ===\")\n    print(f\"Accuracy:    {acc_d:.3f}\")\n    print(f\"AUC:         {auc_d:.3f}\")\n    print(f\"F1:          {f1_d:.3f}\")\n    print(f\"Sensitivity: {sens_d:.3f}\")\n    print(f\"Specificity: {spec_d:.3f}\")\n\n    # ——— 8) 余弦相似度 \n    pred_coef = weights_model.predict(W.T)\n    cos_sim   = 1 - distance.cosine(pred_coef, classifier.coef_[0])\n    print(f\"\\nCosine similarity (word→coef vs CLIP→coef): {cos_sim:.3f}\\n\")\n\n\ndef get_prototypes(df, words, device, save_dir, n_save=20):\n    clip_model, preprocess_fxn = clip.load(\"ViT-B/32\", device=device)\n    X = create_clip_feature_mat(df.file_path.values, clip_model, preprocess_fxn)\n\n    tokened_words = clip.tokenize(words).to(device)\n    with torch.no_grad():\n        word_features = clip_model.encode_text(tokened_words)\n\n    file_dot = np.zeros((len(df), len(words)))\n    for i in range(len(df)):\n        for j in range(len(words)):\n            file_dot[i, j] = np.dot(X[i], word_features[j].cpu())\n\n    file_dot_pred = np.zeros((len(df), len(words)))\n    for j in range(len(words)):\n        fit_j = [k for k in range(len(words)) if k != j]\n        dot_regression = LinearRegression()\n        dot_regression.fit(file_dot[:, fit_j], file_dot[:, j])\n        file_dot_pred[:, j] = dot_regression.predict(file_dot[:, fit_j])\n\n    dot_df_diff = pd.DataFrame(file_dot - file_dot_pred, columns=words)\n    dot_df_diff['label'] = df['label'].values\n    dot_df_diff.set_index(df.file_path, inplace=True)\n\n    for w in words:\n        print(w)\n        for sort_dir in ['top']:\n            this_df = dot_df_diff.sort_values(w, ascending=(sort_dir == 'bottom'))\n            save_files = this_df.index.values[:n_save]\n            these_labels = this_df.label.values[:n_save]\n            this_out_dir = save_dir + w + '_' + sort_dir + '/'\n            if not os.path.exists(this_out_dir):\n                os.mkdir(this_out_dir)\n\n            for i, f in enumerate(save_files):\n                if '.dcm' in f:\n                    im = process_dcm(f)\n                else:\n                    im = imageio.imread(f)\n                    if im.ndim == 2:                        \n                        im = np.stack([im]*3, axis=-1)      \n                    elif im.shape[-1] == 4:                   \n                        im = im[..., :3]\n                # make square and downsample for efficiency \n                min_dim = min(im.shape[:2])\n                for dim in [0, 1]:\n                    if im.shape[dim] > min_dim:\n                        n_start = int((im.shape[dim] - min_dim) / 2)\n                        n_stop = n_start + min_dim\n                        if dim == 0:\n                            im = im[n_start:n_stop, :, :]\n                        else:\n                            im = im[:, n_start:n_stop, :]\n                if min_dim > 500:\n                    im = cv2.resize(im, (500, 500))\n                f_name = f'rank{i}_label{these_labels[i]}.png'\n                imageio.imwrite(os.path.join(this_out_dir, f_name), im)\n\n\nif __name__ == '__main__':\n    dataset_name = 'cbis'\n    device = 'cuda:0'\n\n    if dataset_name == 'cbis':\n        train_df = mass_train[['cropped_image_file_path', 'pathology']].copy()\n        train_df.rename(columns={'cropped_image_file_path': 'file_path'}, inplace=True)\n        train_df['label'] = (train_df['pathology'] == 'MALIGNANT').astype(int)\n        train_df.drop(columns=['pathology'], inplace=True)\n\n        test_df = mass_test[['cropped_image_file_path', 'pathology']].copy()\n        test_df.rename(columns={'cropped_image_file_path': 'file_path'}, inplace=True)\n        test_df['label'] = (test_df['pathology'] == 'MALIGNANT').astype(int)\n        test_df.drop(columns=['pathology'], inplace=True)\n\n    elif dataset_name == 'melanoma':\n        train_df = pd.read_csv('./data/siim_melanoma_train.csv')\n        test_df  = pd.read_csv('./data/siim_melanoma_test.csv')\n\n    words = [\n        'dark', 'light', 'round', 'pointed', 'large', 'small',\n        'smooth', 'coarse', 'transparent', 'opaque',\n        'symmetric', 'asymmetric', 'high contrast', 'low contrast'\n    ]\n\n    base_out_dir = './results/'\n    os.makedirs(base_out_dir, exist_ok=True)\n\n    save_tag = dataset_name\n    save_dir = os.path.join(base_out_dir, save_tag)\n    os.makedirs(save_dir, exist_ok=True)\n\n    fit_words(train_df, test_df, device, words,\n              save_dir=save_dir, save_tag=save_tag)\n\n    prot_save_dir = os.path.join(save_dir, save_tag + '_prototypes')\n    os.makedirs(prot_save_dir, exist_ok=True)\n    get_prototypes(train_df, words, device, prot_save_dir, n_save=5)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:42:19.541971Z","iopub.execute_input":"2025-05-07T21:42:19.542314Z","iopub.status.idle":"2025-05-07T21:43:11.934888Z","shell.execute_reply.started":"2025-05-07T21:42:19.542294Z","shell.execute_reply":"2025-05-07T21:43:11.934194Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CLIP+MLP 微调\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import accuracy_score, roc_auc_score\nfrom tqdm import tqdm\nfrom PIL import Image\n\n# —— 1) 准备数据集 —— \nclass PatchDataset(Dataset):\n    def __init__(self, df, preprocess):\n        self.paths = df.file_path.values\n        self.labels = df.label.values.astype('float32')\n        self.preprocess = preprocess\n    def __len__(self):\n        return len(self.paths)\n    def __getitem__(self, idx):\n        img = Image.open(self.paths[idx]).convert('RGB')\n        x = self.preprocess(img)\n        y = torch.tensor(self.labels[idx], dtype=torch.float32)\n        return x, y\n\n# —— 2) 加载 CLIP backbone & preproces\nclip_model, preprocess_fxn = clip.load(\"ViT-B/32\", device=device)\nclip_model = clip_model.float()  \n\n# Dataset & DataLoader\ntrain_ds = PatchDataset(train_df, preprocess_fxn)\ntest_ds  = PatchDataset(test_df,  preprocess_fxn)\ntrain_loader = DataLoader(train_ds, batch_size=32, shuffle=True,  num_workers=2)\ntest_loader  = DataLoader(test_ds,  batch_size=64, shuffle=False, num_workers=2)\n\n# —— 3) 搭建模型 —— \nclass FineTuneMLP(nn.Module):\n    def __init__(self, backbone, hidden_dim=1024, dropout_p=0.3):\n        super().__init__()\n        self.vision = backbone.visual.float()   # 确保 vision 也是单精度\n        for p in self.vision.parameters():\n            p.requires_grad = False\n\n        d = self.vision.output_dim\n        self.head = nn.Sequential(\n            nn.Linear(d, hidden_dim),\n            nn.ReLU(),\n            nn.Dropout(dropout_p),\n            nn.Linear(hidden_dim, hidden_dim//2),\n            nn.ReLU(),\n            nn.Dropout(dropout_p),\n            nn.Linear(hidden_dim//2, 1)\n        ).float() \n\n    def forward(self, x):\n        f = self.vision(x)         \n        return self.head(f).squeeze(1)  \n\nmodel_ft = FineTuneMLP(clip_model).to(device)\n\n# —— 4) 损失、优化器、调度 —— \ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW([\n    {'params': model_ft.vision.parameters(), 'lr': 5e-6},\n    {'params': model_ft.head.parameters(),   'lr': 2e-4}\n], weight_decay=1e-4)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20)\n\n# —— 5) 训练 —— \nnum_epochs = 20\nfor epoch in range(1, num_epochs+1):\n    model_ft.train()\n    running_loss = 0.0\n    for xb, yb in tqdm(train_loader, desc=f\"Epoch {epoch}/{num_epochs}\"):\n        xb, yb = xb.to(device), yb.to(device)\n        optimizer.zero_grad()\n        logits = model_ft(xb)\n        loss = criterion(logits, yb)\n        loss.backward()\n        optimizer.step()\n        running_loss += loss.item() * xb.size(0)\n    scheduler.step()\n    epoch_loss = running_loss / len(train_loader.dataset)\n    print(f\"Epoch {epoch}/{num_epochs}, Train Loss: {epoch_loss:.4f}\")\n\n# —— 6) 在训练 & 测试集上评估 —— \ndef eval_split(loader):\n    model_ft.eval()\n    ys, ps = [], []\n    with torch.no_grad():\n        for xb, yb in loader:\n            xb = xb.to(device)\n            logit = model_ft(xb).cpu().numpy()\n            prob  = 1 / (1 + np.exp(-logit))\n            ys.append(yb.numpy()); ps.append(prob)\n    y_true = np.concatenate(ys)\n    y_prob = np.concatenate(ps)\n    y_pred = (y_prob >= 0.5).astype(int)\n    return y_true, y_pred, y_prob\n\ny_tr, p_tr, prob_tr = eval_split(train_loader)\ny_te, p_te, prob_te = eval_split(test_loader)\n\nacc_tr = accuracy_score(y_tr, p_tr)\nauc_tr = roc_auc_score(y_tr, prob_tr)\nacc_te = accuracy_score(y_te, p_te)\nauc_te = roc_auc_score(y_te, prob_te)\n\nprint(\"\\n=== [CBIS] CLIP→MLP Improved Performance ===\")\nprint(f\"[Train set] Accuracy: {acc_tr:.3f}, AUC: {auc_tr:.3f}\")\nprint(f\"[Test  set] Accuracy: {acc_te:.3f}, AUC: {auc_te:.3f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:45:59.324162Z","iopub.execute_input":"2025-05-07T21:45:59.324534Z","iopub.status.idle":"2025-05-07T21:47:44.392335Z","shell.execute_reply.started":"2025-05-07T21:45:59.324507Z","shell.execute_reply":"2025-05-07T21:47:44.391242Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# === Shortcut Analysis Inline ===\n\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom sklearn.linear_model import LinearRegression\nfrom scipy.spatial import distance\nfrom PIL import Image\n\n# 原来的14个描述词\norig_words = [\n    'dark', 'light', 'round', 'pointed',\n    'large', 'small', 'smooth', 'coarse',\n    'transparent', 'opaque', 'symmetric', 'asymmetric',\n    'high contrast', 'low contrast'\n]\n# \\要检测的 shortcut 词\nshortcut_words = ['left', 'right', 'CC', 'MLO']\nall_words = orig_words + shortcut_words\n\n# 1) 训练 CLIP→LR\nclip_model, preprocess_fxn = clip.load(\"ViT-B/32\", device=device)\nX_train = create_clip_feature_mat(train_df.file_path.values, clip_model, preprocess_fxn)\nclf = LogisticRegression(random_state=0, C=1, max_iter=1000, fit_intercept=False)\nclf.fit(X_train, train_df.label.values)\n\n# 2) 计算所有词的 embedding 并拟合线性回归以得到权重\ntokened = clip.tokenize(all_words).to(device)\nwith torch.no_grad():\n    word_feats = clip_model.encode_text(tokened).cpu().numpy()   # shape [len(all_words),512]\n\nlm = LinearRegression(fit_intercept=False)\nlm.fit(word_feats.T, clf.coef_[0])\nweights = dict(zip(all_words, lm.coef_))\n\n# 打印所有词的权重及其占比\nabs_w = np.array([abs(weights[w]) for w in all_words])\nprop = abs_w / abs_w.sum()\n# 构造 DataFrame 排序显示\nimport pandas as pd\ndf_w = pd.DataFrame({\n    'word': all_words,\n    'weight': [weights[w] for w in all_words],\n    'prop'  : prop\n})\ndf_w = df_w.sort_values('prop', ascending=False).reset_index(drop=True)\nprint(\"\\n=== All Word Weights and Proportions ===\")\nprint(df_w.to_string(index=False, float_format=\"%.4f\"))\n\n# 3) 打印 shortcut 词的权重\nprint(\"🚦 Shortcut Word Weights 🚦\")\nfor w in shortcut_words:\n    print(f\"{w:>6s} : {weights[w]: .4f}\")\nprint()\n\n# 4) 计算 residuals = dot - dot_pred，用来找 prototypical examples\n#    file_dot[i,j] = X_train[i]·word_feats[j]\nfile_dot = X_train.dot(word_feats.T)  # shape [N_train, len(all_words)]\nfile_dot_pred = np.zeros_like(file_dot)\nfor j in range(len(all_words)):\n    others = [k for k in range(len(all_words)) if k != j]\n    reg = LinearRegression(fit_intercept=False).fit(file_dot[:, others], file_dot[:, j])\n    file_dot_pred[:, j] = reg.predict(file_dot[:, others])\nresiduals = file_dot - file_dot_pred   # shape [N_train, len(all_words)]\n\n# 5) 对每个 shortcut 词，展示 top-3 的训练图像\nTOP_K = 3\nfor w in shortcut_words:\n    j = all_words.index(w)\n    top_idxs = np.argsort(residuals[:, j])[::-1][:TOP_K]\n    print(f\"🔍 Prototypes for “{w}” (label shown under each):\")\n    fig, axes = plt.subplots(1, TOP_K, figsize=(TOP_K*3, 3))\n    for ax, idx in zip(axes, top_idxs):\n        path = train_df.file_path.iloc[idx]\n        img = Image.open(path).convert('RGB')\n        ax.imshow(img)\n        ax.set_title(train_df.label.iloc[idx])\n        ax.axis('off')\n    plt.tight_layout()\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:51:41.530815Z","iopub.execute_input":"2025-05-07T21:51:41.531227Z","iopub.status.idle":"2025-05-07T21:52:06.334701Z","shell.execute_reply.started":"2025-05-07T21:51:41.531192Z","shell.execute_reply":"2025-05-07T21:52:06.333848Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#  Vocabulary Expansion + Full Pipeline \n\n# 1. 定义扩充后的词表\nextended_words = [\n    # 原始通用视觉词\n    'dark', 'light', 'round', 'pointed', 'large', 'small',\n    'smooth', 'coarse', 'transparent', 'opaque',\n    'symmetric', 'asymmetric', 'high contrast', 'low contrast',\n    # 新增医学相关词\n    'spiculated', 'lobulated', 'microlobulated', 'heterogeneous texture',\n    'calcified', 'architectural distortion', 'microcalcifications'\n]\n\n# 2. 重新运行 fit_words，得到新 word_weights\nfit_words(\n    train_df, test_df, device,\n    word_list=extended_words,\n    save_dir=save_dir,\n    save_tag='cbis_extvocab'\n)\n\n\n# 3. 重新加载新权重，打印完整列表\nimport pandas as pd\n\nwd = pd.read_csv(\n    os.path.join(save_dir, 'word_weights-cbis_extvocab.csv'),\n    index_col=0\n)\n\nprint(\"=== Descriptor Weights (extended vocab) ===\")\n# wd 里第一列即是权重，直接 to_string 打印全部词汇\nprint(wd.to_string(float_format=\"%.4f\"))\n\n# 4. 在扩充词表上训练 Descriptor→LR & Descriptor→MLP\n\n# 4.1 构造 descriptor 特征 (重新计算或复用)\nclip_model, preprocess_fxn = clip.load(\"ViT-B/32\", device=device)\nX_train = create_clip_feature_mat(train_df.file_path.values, clip_model, preprocess_fxn)\nX_test  = create_clip_feature_mat(test_df.file_path.values,  clip_model, preprocess_fxn)\n\n# 文本特征\ntokened = clip.tokenize(extended_words).to(device)\nwith torch.no_grad():\n    word_feats = clip_model.encode_text(tokened).cpu().numpy()  # [W,512]\n\n# 点乘得到 descriptor 特征\nXw_train = X_train.dot(word_feats.T)  # [N_train, W]\nXw_test  = X_test.dot(word_feats.T)   # [N_test,  W]\n\n# 4.2 Descriptor→LR\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.metrics import accuracy_score, roc_auc_score\n\nclf2 = LogisticRegression(\n    max_iter=1000,\n    random_state=0,\n    fit_intercept=False\n)\nclf2.fit(Xw_train, train_df.label.values)\n\ny2_tr = clf2.predict(Xw_train)\np2_tr = clf2.predict_proba(Xw_train)[:,1]\ny2_te = clf2.predict(Xw_test)\np2_te = clf2.predict_proba(Xw_test)[:,1]\n\nprint(\"\\n=== Descriptor→LR with extended vocab ===\")\nprint(f\"Train Acc {accuracy_score(train_df.label, y2_tr):.3f}, \"\n      f\"AUC {roc_auc_score(train_df.label, p2_tr):.3f}\")\nprint(f\" Test Acc {accuracy_score(test_df.label, y2_te):.3f}, \"\n      f\"AUC {roc_auc_score(test_df.label, p2_te):.3f}\")\n\n# 4.3 Descriptor→MLP\nfrom sklearn.neural_network import MLPClassifier\n\nmlp2 = MLPClassifier(\n    hidden_layer_sizes=(32,),\n    activation='relu',\n    solver='adam',\n    max_iter=300,\n    random_state=42\n)\nmlp2.fit(Xw_train, train_df.label.values)\n\ny3_tr = mlp2.predict(Xw_train)\np3_tr = mlp2.predict_proba(Xw_train)[:,1]\ny3_te = mlp2.predict(Xw_test)\np3_te = mlp2.predict_proba(Xw_test)[:,1]\n\nprint(\"\\n=== Descriptor→MLP with extended vocab ===\")\nprint(f\"Train Acc {accuracy_score(train_df.label, y3_tr):.3f}, \"\n      f\"AUC {roc_auc_score(train_df.label, p3_tr):.3f}\")\nprint(f\" Test Acc {accuracy_score(test_df.label, y3_te):.3f}, \"\n      f\"AUC {roc_auc_score(test_df.label, p3_te):.3f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:58:24.514712Z","iopub.execute_input":"2025-05-07T21:58:24.515101Z","iopub.status.idle":"2025-05-07T21:59:18.095765Z","shell.execute_reply.started":"2025-05-07T21:58:24.515068Z","shell.execute_reply":"2025-05-07T21:59:18.094982Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import matplotlib.pyplot as plt\n# import seaborn as sns\n# import pandas as pd\n# import numpy as np\n# import os\n\n# def make_word_weights_plot(word_df, base_save_path):\n#     x = word_df.index.values\n#     y = word_df.weights.values\n#     x = np.flipud(x)\n#     y = np.flipud(y)\n\n#     cmap = plt.get_cmap('RdBu_r')\n#     eps = 1e-8\n#     normalized_data = (y - np.min(y)) / (np.max(y) - np.min(y) + eps)\n#     colors = cmap(normalized_data)\n\n#     sns.set_theme()\n#     sns.set_style(\"ticks\")\n\n#     fig, ax = plt.subplots(figsize=(10, 6))\n#     bars = ax.bar(range(len(x)), y, color=colors, edgecolor=\"black\")\n\n#     ax.set_xticks(range(len(x)))\n#     ax.set_xticklabels(x, rotation=60, ha=\"right\", fontweight=\"bold\")\n\n#     yticks = ax.get_yticks()\n#     ax.set_yticks(yticks)\n#     ax.set_yticklabels(np.round(yticks, 2), fontweight=\"bold\")\n\n#     ax.set_ylabel(\"Malignancy Weight\", fontweight=\"bold\", size=14)\n\n#     sns.despine(top=True, right=True)\n#     plt.tight_layout()\n\n#     for ext in ['png', 'pdf']:\n#         plt.savefig(f'{base_save_path}.{ext}', dpi=300, bbox_inches='tight', pad_inches=0.05)\n#     plt.close()\n\n# if __name__ == '__main__':\n#     save_tag = 'cbis'\n#     save_dir = f'./results/{save_tag}/'\n#     word_df = pd.read_csv(save_dir + f'word_weights-{save_tag}.csv', index_col=0)\n#     make_word_weights_plot(word_df, os.path.join(save_dir, f'{save_tag}_weights_plot'))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:35:47.028018Z","iopub.execute_input":"2025-05-07T21:35:47.028348Z","iopub.status.idle":"2025-05-07T21:35:48.497144Z","shell.execute_reply.started":"2025-05-07T21:35:47.028302Z","shell.execute_reply":"2025-05-07T21:35:48.496248Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n\n# save_tag = 'cbis'\n# save_dir = f'./results/{save_tag}/'\n# print(\"PNG exists:\", os.path.exists(os.path.join(save_dir, f'{save_tag}_weights_plot.png')))\n# print(\"PDF exists:\", os.path.exists(os.path.join(save_dir, f'{save_tag}_weights_plot.pdf')))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-07T21:35:50.471881Z","iopub.execute_input":"2025-05-07T21:35:50.472457Z","iopub.status.idle":"2025-05-07T21:35:50.478425Z","shell.execute_reply.started":"2025-05-07T21:35:50.472428Z","shell.execute_reply":"2025-05-07T21:35:50.477537Z"}},"outputs":[],"execution_count":null}]}